From 1a9159daff5fabfb3419a9cf728434d9a8e5d876 Mon Sep 17 00:00:00 2001 From: JohnnyMorganz Date: Thu, 2 Nov 2023 16:14:51 +0000 Subject: [PATCH 01/19] Record table type alias property locations (#1046) For table type aliases, records the location of the property in the alias declaration. This is an alternate solution to the particular case noted in #802. Instead of tracking the type alias definition for FTVs, it tracks it for the encompassing property instead. Note that we still don't have positions in the following case: ``` type Func = () -> () ``` Closes #802 (Although not completely...) --- Analysis/include/Luau/Type.h | 3 ++- Analysis/src/Type.cpp | 3 ++- Analysis/src/TypeInfer.cpp | 2 +- tests/TypeInfer.aliases.test.cpp | 23 +++++++++++++++++++++++ 4 files changed, 28 insertions(+), 3 deletions(-) diff --git a/Analysis/include/Luau/Type.h b/Analysis/include/Luau/Type.h index 9b8564fb..959ec49a 100644 --- a/Analysis/include/Luau/Type.h +++ b/Analysis/include/Luau/Type.h @@ -374,6 +374,7 @@ struct Property bool deprecated = false; std::string deprecatedSuggestion; std::optional location = std::nullopt; + std::optional typeLocation = std::nullopt; Tags tags; std::optional documentationSymbol; @@ -381,7 +382,7 @@ struct Property // TODO: Kill all constructors in favor of `Property::rw(TypeId read, TypeId write)` and friends. Property(); Property(TypeId readTy, bool deprecated = false, const std::string& deprecatedSuggestion = "", std::optional location = std::nullopt, - const Tags& tags = {}, const std::optional& documentationSymbol = std::nullopt); + const Tags& tags = {}, const std::optional& documentationSymbol = std::nullopt, std::optional typeLocation = std::nullopt); // DEPRECATED: Should only be called in non-RWP! We assert that the `readTy` is not nullopt. // TODO: Kill once we don't have non-RWP. diff --git a/Analysis/src/Type.cpp b/Analysis/src/Type.cpp index 1859131b..2f98e85a 100644 --- a/Analysis/src/Type.cpp +++ b/Analysis/src/Type.cpp @@ -604,10 +604,11 @@ FunctionType::FunctionType(TypeLevel level, Scope* scope, std::vector ge Property::Property() {} Property::Property(TypeId readTy, bool deprecated, const std::string& deprecatedSuggestion, std::optional location, const Tags& tags, - const std::optional& documentationSymbol) + const std::optional& documentationSymbol, std::optional typeLocation) : deprecated(deprecated) , deprecatedSuggestion(deprecatedSuggestion) , location(location) + , typeLocation(typeLocation) , tags(tags) , documentationSymbol(documentationSymbol) , readTy(readTy) diff --git a/Analysis/src/TypeInfer.cpp b/Analysis/src/TypeInfer.cpp index a29b1e06..1ff8b30b 100644 --- a/Analysis/src/TypeInfer.cpp +++ b/Analysis/src/TypeInfer.cpp @@ -5403,7 +5403,7 @@ TypeId TypeChecker::resolveTypeWorker(const ScopePtr& scope, const AstType& anno std::optional tableIndexer; for (const auto& prop : table->props) - props[prop.name.value] = {resolveType(scope, *prop.type)}; + props[prop.name.value] = {resolveType(scope, *prop.type), /* deprecated: */ false, {}, std::nullopt, {}, std::nullopt, prop.location}; if (const auto& indexer = table->indexer) tableIndexer = TableIndexer(resolveType(scope, *indexer->indexType), resolveType(scope, *indexer->resultType)); diff --git a/tests/TypeInfer.aliases.test.cpp b/tests/TypeInfer.aliases.test.cpp index 3f6d90fa..de273e98 100644 --- a/tests/TypeInfer.aliases.test.cpp +++ b/tests/TypeInfer.aliases.test.cpp @@ -1037,4 +1037,27 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "alias_expands_to_bare_reference_to_imported_ LUAU_REQUIRE_NO_ERRORS(result); } +TEST_CASE_FIXTURE(Fixture, "table_types_record_the_property_locations") +{ + CheckResult result = check(R"( + type Table = { + create: () -> () + } + + local x: Table + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + auto ty = requireTypeAlias("Table"); + + auto ttv = Luau::get(ty); + REQUIRE(ttv); + + auto propIt = ttv->props.find("create"); + REQUIRE(propIt != ttv->props.end()); + + CHECK_EQ(propIt->second.location, std::nullopt); + CHECK_EQ(propIt->second.typeLocation, Location({2, 12}, {2, 18})); +} + TEST_SUITE_END(); From 7105c815797cbc5404566db22b807b0579b75ca5 Mon Sep 17 00:00:00 2001 From: aaron Date: Fri, 3 Nov 2023 16:45:04 -0700 Subject: [PATCH 02/19] Sync to upstream/release/602 (#1089) # What's changed? * Fixed a bug in type cloning by maintaining persistent types. * We now parse imprecise integer literals to report the imprecision as a warning to developers. * Add a compiler flag to specify the name of the statistics output file. ### New type solver * Renamed `ConstraintGraphBuilder` to `ConstraintGenerator` * LValues now take into account the type being assigned during constraint generation. * Normalization performance has been improved by 33% by replacing the an internal usage of `std::unordered_set` with `DenseHashMap`. * Normalization now has a helper to identify types that are equivalent to `unknown`, which is being used to fix some bugs in subtyping. * Uses of the old unifier in the new type solver have been eliminated. * Improved error explanations for subtyping errors in `TypeChecker2`. ### Native code generation * Expanded some of the statistics recorded during compilation to include the number of instructions and blocks. * Introduce instruction and block count limiters for controlling what bytecode is translated into native code. * Implement code generation for byteswap instruction. ### Internal Contributors Co-authored-by: Aaron Weiss Co-authored-by: Alexander McCord Co-authored-by: Andy Friesen Co-authored-by: Aviral Goel Co-authored-by: Lily Brown --- ...ntGraphBuilder.h => ConstraintGenerator.h} | 26 +- Analysis/include/Luau/DataFlowGraph.h | 4 +- Analysis/include/Luau/Normalize.h | 3 +- Analysis/include/Luau/TypeChecker2.h | 4 +- Analysis/include/Luau/TypeOrPack.h | 44 ++-- Analysis/src/Autocomplete.cpp | 23 +- Analysis/src/BuiltinDefinitions.cpp | 2 +- Analysis/src/Clone.cpp | 12 +- ...aphBuilder.cpp => ConstraintGenerator.cpp} | 245 ++++++++---------- Analysis/src/DataFlowGraph.cpp | 10 +- Analysis/src/Frontend.cpp | 12 +- Analysis/src/Linter.cpp | 19 +- Analysis/src/Module.cpp | 2 +- Analysis/src/Normalize.cpp | 141 ++++++++-- Analysis/src/Subtyping.cpp | 14 +- Analysis/src/TypeChecker2.cpp | 47 ++-- Analysis/src/TypeInfer.cpp | 25 +- Analysis/src/Unifier.cpp | 10 +- Ast/include/Luau/Ast.h | 1 + Ast/include/Luau/Location.h | 34 ++- Ast/src/Location.cpp | 31 --- Ast/src/Parser.cpp | 36 ++- CLI/Compile.cpp | 55 +++- CodeGen/include/Luau/CodeGen.h | 24 ++ CodeGen/include/Luau/IrData.h | 4 + CodeGen/src/CodeGenLower.h | 25 ++ CodeGen/src/CodeGenUtils.cpp | 92 +------ CodeGen/src/CodeGenUtils.h | 1 - CodeGen/src/IrDump.cpp | 2 + CodeGen/src/IrLoweringA64.cpp | 7 + CodeGen/src/IrLoweringX64.cpp | 24 +- CodeGen/src/IrTranslateBuiltins.cpp | 10 +- CodeGen/src/IrUtils.cpp | 1 + CodeGen/src/NativeState.cpp | 1 - CodeGen/src/NativeState.h | 1 - CodeGen/src/OptimizeConstProp.cpp | 1 + Compiler/include/Luau/BytecodeBuilder.h | 2 + Compiler/src/BytecodeBuilder.cpp | 6 + Compiler/src/Compiler.cpp | 65 +---- Sources.cmake | 8 +- VM/src/lbuiltins.cpp | 18 +- VM/src/ldo.cpp | 4 +- bench/tests/sha256.lua | 3 +- tests/Compiler.test.cpp | 9 - tests/Conformance.test.cpp | 2 - ...ure.cpp => ConstraintGeneratorFixture.cpp} | 16 +- ...Fixture.h => ConstraintGeneratorFixture.h} | 8 +- tests/ConstraintSolver.test.cpp | 8 +- tests/Error.test.cpp | 2 +- tests/IrBuilder.test.cpp | 34 +++ tests/Linter.test.cpp | 71 ++++- tests/Module.test.cpp | 38 ++- tests/Normalize.test.cpp | 44 +++- tests/Subtyping.test.cpp | 9 + tests/TypeInfer.aliases.test.cpp | 6 +- tests/TypeInfer.cfa.test.cpp | 2 +- tests/TypeInfer.classes.test.cpp | 4 - tests/TypeInfer.intersectionTypes.test.cpp | 2 - tests/TypeInfer.oop.test.cpp | 2 +- tests/TypeInfer.singletons.test.cpp | 2 - tests/TypeInfer.tables.test.cpp | 34 ++- tests/TypeInfer.test.cpp | 2 - tests/TypeInfer.tryUnify.test.cpp | 39 ++- tests/TypePath.test.cpp | 2 - tests/conformance/bitwise.lua | 5 + tests/conformance/math.lua | 1 + tests/conformance/utf8.lua | 69 +++-- tests/main.cpp | 65 +++++ tools/faillist.txt | 17 +- 69 files changed, 1013 insertions(+), 579 deletions(-) rename Analysis/include/Luau/{ConstraintGraphBuilder.h => ConstraintGenerator.h} (94%) rename Analysis/src/{ConstraintGraphBuilder.cpp => ConstraintGenerator.cpp} (92%) rename tests/{ConstraintGraphBuilderFixture.cpp => ConstraintGeneratorFixture.cpp} (62%) rename tests/{ConstraintGraphBuilderFixture.h => ConstraintGeneratorFixture.h} (81%) diff --git a/Analysis/include/Luau/ConstraintGraphBuilder.h b/Analysis/include/Luau/ConstraintGenerator.h similarity index 94% rename from Analysis/include/Luau/ConstraintGraphBuilder.h rename to Analysis/include/Luau/ConstraintGenerator.h index bba5ebd9..088ae4c2 100644 --- a/Analysis/include/Luau/ConstraintGraphBuilder.h +++ b/Analysis/include/Luau/ConstraintGenerator.h @@ -57,7 +57,7 @@ struct InferencePack } }; -struct ConstraintGraphBuilder +struct ConstraintGenerator { // A list of all the scopes in the module. This vector holds ownership of the // scope pointers; the scopes themselves borrow pointers to other scopes to @@ -68,7 +68,7 @@ struct ConstraintGraphBuilder NotNull builtinTypes; const NotNull arena; // The root scope of the module we're generating constraints for. - // This is null when the CGB is initially constructed. + // This is null when the CG is initially constructed. Scope* rootScope; struct InferredBinding @@ -116,13 +116,13 @@ struct ConstraintGraphBuilder DcrLogger* logger; - ConstraintGraphBuilder(ModulePtr module, NotNull normalizer, NotNull moduleResolver, + ConstraintGenerator(ModulePtr module, NotNull normalizer, NotNull moduleResolver, NotNull builtinTypes, NotNull ice, const ScopePtr& globalScope, std::function prepareModuleScope, DcrLogger* logger, NotNull dfg, std::vector requireCycles); /** - * The entry point to the ConstraintGraphBuilder. This will construct a set + * The entry point to the ConstraintGenerator. This will construct a set * of scopes, constraints, and free types that can be solved later. * @param block the root block to generate constraints for. */ @@ -232,12 +232,16 @@ private: Inference check(const ScopePtr& scope, AstExprTable* expr, std::optional expectedType); std::tuple checkBinary(const ScopePtr& scope, AstExprBinary* binary, std::optional expectedType); - std::optional checkLValue(const ScopePtr& scope, AstExpr* expr); - std::optional checkLValue(const ScopePtr& scope, AstExprLocal* local); - std::optional checkLValue(const ScopePtr& scope, AstExprGlobal* global); - std::optional checkLValue(const ScopePtr& scope, AstExprIndexName* indexName); - std::optional checkLValue(const ScopePtr& scope, AstExprIndexExpr* indexExpr); - TypeId updateProperty(const ScopePtr& scope, AstExpr* expr); + /** + * Generate constraints to assign assignedTy to the expression expr + * @returns the type of the expression. This may or may not be assignedTy itself. + */ + std::optional checkLValue(const ScopePtr& scope, AstExpr* expr, TypeId assignedTy); + std::optional checkLValue(const ScopePtr& scope, AstExprLocal* local, TypeId assignedTy); + std::optional checkLValue(const ScopePtr& scope, AstExprGlobal* global, TypeId assignedTy); + std::optional checkLValue(const ScopePtr& scope, AstExprIndexName* indexName, TypeId assignedTy); + std::optional checkLValue(const ScopePtr& scope, AstExprIndexExpr* indexExpr, TypeId assignedTy); + TypeId updateProperty(const ScopePtr& scope, AstExpr* expr, TypeId assignedTy); void updateLValueType(AstExpr* lvalue, TypeId ty); @@ -324,7 +328,7 @@ private: /** Scan the program for global definitions. * - * ConstraintGraphBuilder needs to differentiate between globals and accesses to undefined symbols. Doing this "for + * ConstraintGenerator needs to differentiate between globals and accesses to undefined symbols. Doing this "for * real" in a general way is going to be pretty hard, so we are choosing not to tackle that yet. For now, we do an * initial scan of the AST and note what globals are defined. */ diff --git a/Analysis/include/Luau/DataFlowGraph.h b/Analysis/include/Luau/DataFlowGraph.h index 34a0484a..f752f022 100644 --- a/Analysis/include/Luau/DataFlowGraph.h +++ b/Analysis/include/Luau/DataFlowGraph.h @@ -34,7 +34,7 @@ struct DataFlowGraph DataFlowGraph& operator=(DataFlowGraph&&) = default; DefId getDef(const AstExpr* expr) const; - // Look up for the rvalue breadcrumb for a compound assignment. + // Look up for the rvalue def for a compound assignment. std::optional getRValueDefForCompoundAssign(const AstExpr* expr) const; DefId getDef(const AstLocal* local) const; @@ -64,7 +64,7 @@ private: // Compound assignments are in a weird situation where the local being assigned to is also being used at its // previous type implicitly in an rvalue position. This map provides the previous binding. - DenseHashMap compoundAssignBreadcrumbs{nullptr}; + DenseHashMap compoundAssignDefs{nullptr}; DenseHashMap astRefinementKeys{nullptr}; diff --git a/Analysis/include/Luau/Normalize.h b/Analysis/include/Luau/Normalize.h index ebb80e0f..54a4dc61 100644 --- a/Analysis/include/Luau/Normalize.h +++ b/Analysis/include/Luau/Normalize.h @@ -29,7 +29,7 @@ bool isConsistentSubtype(TypePackId subTy, TypePackId superTy, NotNull sc class TypeIds { private: - std::unordered_set types; + DenseHashMap types{nullptr}; std::vector order; std::size_t hash = 0; @@ -277,6 +277,7 @@ struct NormalizedType NormalizedType& operator=(NormalizedType&&) = default; // IsType functions + bool isUnknown() const; /// Returns true if the type is exactly a number. Behaves like Type::isNumber() bool isExactlyNumber() const; diff --git a/Analysis/include/Luau/TypeChecker2.h b/Analysis/include/Luau/TypeChecker2.h index aeeab0f8..b30cfe01 100644 --- a/Analysis/include/Luau/TypeChecker2.h +++ b/Analysis/include/Luau/TypeChecker2.h @@ -2,8 +2,6 @@ #pragma once -#include "Luau/Ast.h" -#include "Luau/Module.h" #include "Luau/NotNull.h" namespace Luau @@ -13,6 +11,8 @@ struct BuiltinTypes; struct DcrLogger; struct TypeCheckLimits; struct UnifierSharedState; +struct SourceModule; +struct Module; void check(NotNull builtinTypes, NotNull sharedState, NotNull limits, DcrLogger* logger, const SourceModule& sourceModule, Module* module); diff --git a/Analysis/include/Luau/TypeOrPack.h b/Analysis/include/Luau/TypeOrPack.h index 2bdca1df..87001910 100644 --- a/Analysis/include/Luau/TypeOrPack.h +++ b/Analysis/include/Luau/TypeOrPack.h @@ -12,32 +12,28 @@ namespace Luau const void* ptr(TypeOrPack ty); -template -const T* get(TypeOrPack ty) +template, bool> = true> +const T* get(const TypeOrPack& tyOrTp) { - if constexpr (std::is_same_v) - return ty.get_if(); - else if constexpr (std::is_same_v) - return ty.get_if(); - else if constexpr (TypeVariant::is_part_of_v) - { - if (auto innerTy = ty.get_if()) - return get(*innerTy); - else - return nullptr; - } - else if constexpr (TypePackVariant::is_part_of_v) - { - if (auto innerTp = ty.get_if()) - return get(*innerTp); - else - return nullptr; - } + return tyOrTp.get_if(); +} + +template, bool> = true> +const T* get(const TypeOrPack& tyOrTp) +{ + if (const TypeId* ty = get(tyOrTp)) + return get(*ty); else - { - static_assert(always_false_v, "invalid T to get from TypeOrPack"); - LUAU_UNREACHABLE(); - } + return nullptr; +} + +template, bool> = true> +const T* get(const TypeOrPack& tyOrTp) +{ + if (const TypePackId* tp = get(tyOrTp)) + return get(*tp); + else + return nullptr; } TypeOrPack follow(TypeOrPack ty); diff --git a/Analysis/src/Autocomplete.cpp b/Analysis/src/Autocomplete.cpp index d73598c7..52cb54e3 100644 --- a/Analysis/src/Autocomplete.cpp +++ b/Analysis/src/Autocomplete.cpp @@ -5,6 +5,7 @@ #include "Luau/BuiltinDefinitions.h" #include "Luau/Frontend.h" #include "Luau/ToString.h" +#include "Luau/Subtyping.h" #include "Luau/TypeInfer.h" #include "Luau/TypePack.h" @@ -12,6 +13,7 @@ #include #include +LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution); LUAU_FASTFLAG(DebugLuauReadWriteProperties); LUAU_FASTFLAG(LuauClipExtraHasEndProps); LUAU_FASTFLAGVARIABLE(LuauAutocompleteDoEnd, false); @@ -143,13 +145,24 @@ static bool checkTypeMatch(TypeId subTy, TypeId superTy, NotNull scope, T InternalErrorReporter iceReporter; UnifierSharedState unifierState(&iceReporter); Normalizer normalizer{typeArena, builtinTypes, NotNull{&unifierState}}; - Unifier unifier(NotNull{&normalizer}, scope, Location(), Variance::Covariant); - // Cost of normalization can be too high for autocomplete response time requirements - unifier.normalize = false; - unifier.checkInhabited = false; + if (FFlag::DebugLuauDeferredConstraintResolution) + { + Subtyping subtyping{builtinTypes, NotNull{typeArena}, NotNull{&normalizer}, NotNull{&iceReporter}, scope}; + + return subtyping.isSubtype(subTy, superTy).isSubtype; + } + else + { + Unifier unifier(NotNull{&normalizer}, scope, Location(), Variance::Covariant); + + // Cost of normalization can be too high for autocomplete response time requirements + unifier.normalize = false; + unifier.checkInhabited = false; + + return unifier.canUnify(subTy, superTy).empty(); + } - return unifier.canUnify(subTy, superTy).empty(); } static TypeCorrectKind checkTypeCorrectKind( diff --git a/Analysis/src/BuiltinDefinitions.cpp b/Analysis/src/BuiltinDefinitions.cpp index b7631460..5ce12873 100644 --- a/Analysis/src/BuiltinDefinitions.cpp +++ b/Analysis/src/BuiltinDefinitions.cpp @@ -7,7 +7,7 @@ #include "Luau/Common.h" #include "Luau/ToString.h" #include "Luau/ConstraintSolver.h" -#include "Luau/ConstraintGraphBuilder.h" +#include "Luau/ConstraintGenerator.h" #include "Luau/NotNull.h" #include "Luau/TypeInfer.h" #include "Luau/TypeFamily.h" diff --git a/Analysis/src/Clone.cpp b/Analysis/src/Clone.cpp index 01b0bdfd..a0e76987 100644 --- a/Analysis/src/Clone.cpp +++ b/Analysis/src/Clone.cpp @@ -14,7 +14,7 @@ LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution) LUAU_FASTINTVARIABLE(LuauTypeCloneRecursionLimit, 300) LUAU_FASTFLAGVARIABLE(LuauCloneCyclicUnions, false) -LUAU_FASTFLAGVARIABLE(LuauStacklessTypeClone2, false) +LUAU_FASTFLAGVARIABLE(LuauStacklessTypeClone3, false) LUAU_FASTINTVARIABLE(LuauTypeCloneIterationLimit, 100'000) namespace Luau @@ -118,6 +118,8 @@ private: ty = follow(ty, FollowOption::DisableLazyTypeThunks); if (auto it = types->find(ty); it != types->end()) return it->second; + else if (ty->persistent) + return ty; return std::nullopt; } @@ -126,6 +128,8 @@ private: tp = follow(tp); if (auto it = packs->find(tp); it != packs->end()) return it->second; + else if (tp->persistent) + return tp; return std::nullopt; } @@ -879,7 +883,7 @@ TypePackId clone(TypePackId tp, TypeArena& dest, CloneState& cloneState) if (tp->persistent) return tp; - if (FFlag::LuauStacklessTypeClone2) + if (FFlag::LuauStacklessTypeClone3) { TypeCloner2 cloner{NotNull{&dest}, cloneState.builtinTypes, NotNull{&cloneState.seenTypes}, NotNull{&cloneState.seenTypePacks}}; return cloner.clone(tp); @@ -905,7 +909,7 @@ TypeId clone(TypeId typeId, TypeArena& dest, CloneState& cloneState) if (typeId->persistent) return typeId; - if (FFlag::LuauStacklessTypeClone2) + if (FFlag::LuauStacklessTypeClone3) { TypeCloner2 cloner{NotNull{&dest}, cloneState.builtinTypes, NotNull{&cloneState.seenTypes}, NotNull{&cloneState.seenTypePacks}}; return cloner.clone(typeId); @@ -934,7 +938,7 @@ TypeId clone(TypeId typeId, TypeArena& dest, CloneState& cloneState) TypeFun clone(const TypeFun& typeFun, TypeArena& dest, CloneState& cloneState) { - if (FFlag::LuauStacklessTypeClone2) + if (FFlag::LuauStacklessTypeClone3) { TypeCloner2 cloner{NotNull{&dest}, cloneState.builtinTypes, NotNull{&cloneState.seenTypes}, NotNull{&cloneState.seenTypePacks}}; diff --git a/Analysis/src/ConstraintGraphBuilder.cpp b/Analysis/src/ConstraintGenerator.cpp similarity index 92% rename from Analysis/src/ConstraintGraphBuilder.cpp rename to Analysis/src/ConstraintGenerator.cpp index 4f4ff306..e5652549 100644 --- a/Analysis/src/ConstraintGraphBuilder.cpp +++ b/Analysis/src/ConstraintGenerator.cpp @@ -1,5 +1,5 @@ // This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details -#include "Luau/ConstraintGraphBuilder.h" +#include "Luau/ConstraintGenerator.h" #include "Luau/Ast.h" #include "Luau/Def.h" @@ -126,21 +126,21 @@ struct Checkpoint size_t offset; }; -Checkpoint checkpoint(const ConstraintGraphBuilder* cgb) +Checkpoint checkpoint(const ConstraintGenerator* cg) { - return Checkpoint{cgb->constraints.size()}; + return Checkpoint{cg->constraints.size()}; } template -void forEachConstraint(const Checkpoint& start, const Checkpoint& end, const ConstraintGraphBuilder* cgb, F f) +void forEachConstraint(const Checkpoint& start, const Checkpoint& end, const ConstraintGenerator* cg, F f) { for (size_t i = start.offset; i < end.offset; ++i) - f(cgb->constraints[i]); + f(cg->constraints[i]); } } // namespace -ConstraintGraphBuilder::ConstraintGraphBuilder(ModulePtr module, NotNull normalizer, NotNull moduleResolver, +ConstraintGenerator::ConstraintGenerator(ModulePtr module, NotNull normalizer, NotNull moduleResolver, NotNull builtinTypes, NotNull ice, const ScopePtr& globalScope, std::function prepareModuleScope, DcrLogger* logger, NotNull dfg, std::vector requireCycles) @@ -160,7 +160,7 @@ ConstraintGraphBuilder::ConstraintGraphBuilder(ModulePtr module, NotNullcaptureGenerationModule(module); } -TypeId ConstraintGraphBuilder::freshType(const ScopePtr& scope) +TypeId ConstraintGenerator::freshType(const ScopePtr& scope) { return Luau::freshType(arena, builtinTypes, scope.get()); } -TypePackId ConstraintGraphBuilder::freshTypePack(const ScopePtr& scope) +TypePackId ConstraintGenerator::freshTypePack(const ScopePtr& scope) { FreeTypePack f{scope.get()}; return arena->addTypePack(TypePackVar{std::move(f)}); } -ScopePtr ConstraintGraphBuilder::childScope(AstNode* node, const ScopePtr& parent) +ScopePtr ConstraintGenerator::childScope(AstNode* node, const ScopePtr& parent) { auto scope = std::make_shared(parent); scopes.emplace_back(node->location, scope); @@ -206,17 +206,17 @@ ScopePtr ConstraintGraphBuilder::childScope(AstNode* node, const ScopePtr& paren return scope; } -NotNull ConstraintGraphBuilder::addConstraint(const ScopePtr& scope, const Location& location, ConstraintV cv) +NotNull ConstraintGenerator::addConstraint(const ScopePtr& scope, const Location& location, ConstraintV cv) { return NotNull{constraints.emplace_back(new Constraint{NotNull{scope.get()}, location, std::move(cv)}).get()}; } -NotNull ConstraintGraphBuilder::addConstraint(const ScopePtr& scope, std::unique_ptr c) +NotNull ConstraintGenerator::addConstraint(const ScopePtr& scope, std::unique_ptr c) { return NotNull{constraints.emplace_back(std::move(c)).get()}; } -void ConstraintGraphBuilder::unionRefinements(const RefinementContext& lhs, const RefinementContext& rhs, RefinementContext& dest, std::vector* constraints) +void ConstraintGenerator::unionRefinements(const RefinementContext& lhs, const RefinementContext& rhs, RefinementContext& dest, std::vector* constraints) { const auto intersect = [&](const std::vector& types) { if (1 == types.size()) @@ -252,7 +252,7 @@ void ConstraintGraphBuilder::unionRefinements(const RefinementContext& lhs, cons } } -void ConstraintGraphBuilder::computeRefinement(const ScopePtr& scope, RefinementId refinement, RefinementContext* refis, bool sense, bool eq, std::vector* constraints) +void ConstraintGenerator::computeRefinement(const ScopePtr& scope, RefinementId refinement, RefinementContext* refis, bool sense, bool eq, std::vector* constraints) { if (!refinement) return; @@ -382,7 +382,7 @@ bool mustDeferIntersection(TypeId ty) } } // namespace -void ConstraintGraphBuilder::applyRefinements(const ScopePtr& scope, Location location, RefinementId refinement) +void ConstraintGenerator::applyRefinements(const ScopePtr& scope, Location location, RefinementId refinement) { if (!refinement) return; @@ -439,7 +439,7 @@ void ConstraintGraphBuilder::applyRefinements(const ScopePtr& scope, Location lo addConstraint(scope, location, c); } -ControlFlow ConstraintGraphBuilder::visitBlockWithoutChildScope(const ScopePtr& scope, AstStatBlock* block) +ControlFlow ConstraintGenerator::visitBlockWithoutChildScope(const ScopePtr& scope, AstStatBlock* block) { RecursionCounter counter{&recursionCount}; @@ -502,7 +502,7 @@ ControlFlow ConstraintGraphBuilder::visitBlockWithoutChildScope(const ScopePtr& return firstControlFlow.value_or(ControlFlow::None); } -ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStat* stat) +ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStat* stat) { RecursionLimiter limiter{&recursionCount, FInt::LuauCheckRecursionLimit}; @@ -560,7 +560,7 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStat* stat) } } -ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatLocal* statLocal) +ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatLocal* statLocal) { std::vector> varTypes; varTypes.reserve(statLocal->vars.size); @@ -663,7 +663,7 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatLocal* s return ControlFlow::None; } -ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatFor* for_) +ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatFor* for_) { TypeId annotationTy = builtinTypes->numberType; if (for_->var->annotation) @@ -693,7 +693,7 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatFor* for return ControlFlow::None; } -ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatForIn* forIn) +ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatForIn* forIn) { ScopePtr loopScope = childScope(forIn, scope); @@ -728,7 +728,7 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatForIn* f return ControlFlow::None; } -ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatWhile* while_) +ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatWhile* while_) { RefinementId refinement = check(scope, while_->condition).refinement; @@ -740,7 +740,7 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatWhile* w return ControlFlow::None; } -ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatRepeat* repeat) +ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatRepeat* repeat) { ScopePtr repeatScope = childScope(repeat, scope); @@ -751,7 +751,7 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatRepeat* return ControlFlow::None; } -ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatLocalFunction* function) +ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatLocalFunction* function) { // Local // Global @@ -801,7 +801,7 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatLocalFun return ControlFlow::None; } -ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatFunction* function) +ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatFunction* function) { // Name could be AstStatLocal, AstStatGlobal, AstStatIndexName. // With or without self @@ -846,7 +846,7 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatFunction else if (AstExprIndexName* indexName = function->name->as()) { Checkpoint check1 = checkpoint(this); - std::optional lvalueType = checkLValue(scope, indexName); + std::optional lvalueType = checkLValue(scope, indexName, generalizedType); LUAU_ASSERT(lvalueType); Checkpoint check2 = checkpoint(this); @@ -856,12 +856,9 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatFunction // TODO figure out how to populate the location field of the table Property. - if (lvalueType) + if (lvalueType && *lvalueType != generalizedType) { - if (get(*lvalueType)) - asMutable(*lvalueType)->ty.emplace(generalizedType); - else - addConstraint(scope, indexName->location, SubtypeConstraint{*lvalueType, generalizedType}); + addConstraint(scope, indexName->location, SubtypeConstraint{*lvalueType, generalizedType}); } } else if (AstExprError* err = function->name->as()) @@ -900,7 +897,7 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatFunction return ControlFlow::None; } -ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatReturn* ret) +ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatReturn* ret) { // At this point, the only way scope->returnType should have anything // interesting in it is if the function has an explicit return annotation. @@ -916,7 +913,7 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatReturn* return ControlFlow::Returns; } -ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatBlock* block) +ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatBlock* block) { ScopePtr innerScope = childScope(block, scope); @@ -944,7 +941,7 @@ static void bindFreeType(TypeId a, TypeId b) asMutable(b)->ty.emplace(a); } -ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatAssign* assign) +ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatAssign* assign) { std::vector> expectedTypes; expectedTypes.reserve(assign->vars.size); @@ -957,16 +954,7 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatAssign* TypeId assignee = arena->addType(BlockedType{}); assignees.push_back(assignee); - std::optional upperBound = follow(checkLValue(scope, lvalue)); - if (upperBound) - { - if (get(*upperBound)) - expectedTypes.push_back(std::nullopt); - else - expectedTypes.push_back(*upperBound); - - addConstraint(scope, lvalue->location, SubtypeConstraint{assignee, *upperBound}); - } + checkLValue(scope, lvalue, assignee); DefId def = dfg->getDef(lvalue); scope->lvalueTypes[def] = assignee; @@ -979,14 +967,12 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatAssign* return ControlFlow::None; } -ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatCompoundAssign* assign) +ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatCompoundAssign* assign) { - std::optional varTy = checkLValue(scope, assign->var); - AstExprBinary binop = AstExprBinary{assign->location, assign->op, assign->var, assign->value}; TypeId resultTy = check(scope, &binop).ty; - if (varTy) - addConstraint(scope, assign->location, SubtypeConstraint{resultTy, *varTy}); + + checkLValue(scope, assign->var, resultTy); DefId def = dfg->getDef(assign->var); scope->lvalueTypes[def] = resultTy; @@ -994,7 +980,7 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatCompound return ControlFlow::None; } -ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatIf* ifStatement) +ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatIf* ifStatement) { RefinementId refinement = check(scope, ifStatement->condition, std::nullopt).refinement; @@ -1041,7 +1027,7 @@ static bool occursCheck(TypeId needle, TypeId haystack) return false; } -ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatTypeAlias* alias) +ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatTypeAlias* alias) { ScopePtr* defnScope = astTypeAliasDefiningScopes.find(alias); @@ -1090,7 +1076,7 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatTypeAlia return ControlFlow::None; } -ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatDeclareGlobal* global) +ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatDeclareGlobal* global) { LUAU_ASSERT(global->type); @@ -1115,7 +1101,7 @@ static bool isMetamethod(const Name& name) (FFlag::LuauFloorDivision && name == "__idiv"); } -ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatDeclareClass* declaredClass) +ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatDeclareClass* declaredClass) { std::optional superTy = std::make_optional(builtinTypes->classType); if (declaredClass->superName) @@ -1234,7 +1220,7 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatDeclareC return ControlFlow::None; } -ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatDeclareFunction* global) +ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatDeclareFunction* global) { std::vector> generics = createGenerics(scope, global->generics); std::vector> genericPacks = createGenericPacks(scope, global->genericPacks); @@ -1279,7 +1265,7 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatDeclareF return ControlFlow::None; } -ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatError* error) +ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatError* error) { for (AstStat* stat : error->statements) visit(scope, stat); @@ -1289,7 +1275,7 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatError* e return ControlFlow::None; } -InferencePack ConstraintGraphBuilder::checkPack( +InferencePack ConstraintGenerator::checkPack( const ScopePtr& scope, AstArray exprs, const std::vector>& expectedTypes) { std::vector head; @@ -1320,7 +1306,7 @@ InferencePack ConstraintGraphBuilder::checkPack( return InferencePack{arena->addTypePack(TypePack{std::move(head), tail})}; } -InferencePack ConstraintGraphBuilder::checkPack( +InferencePack ConstraintGenerator::checkPack( const ScopePtr& scope, AstExpr* expr, const std::vector>& expectedTypes, bool generalize) { RecursionCounter counter{&recursionCount}; @@ -1356,7 +1342,7 @@ InferencePack ConstraintGraphBuilder::checkPack( return result; } -InferencePack ConstraintGraphBuilder::checkPack(const ScopePtr& scope, AstExprCall* call) +InferencePack ConstraintGenerator::checkPack(const ScopePtr& scope, AstExprCall* call) { std::vector exprArgs; @@ -1530,7 +1516,7 @@ InferencePack ConstraintGraphBuilder::checkPack(const ScopePtr& scope, AstExprCa } } -Inference ConstraintGraphBuilder::check( +Inference ConstraintGenerator::check( const ScopePtr& scope, AstExpr* expr, std::optional expectedType, bool forceSingleton, bool generalize) { RecursionCounter counter{&recursionCount}; @@ -1600,7 +1586,7 @@ Inference ConstraintGraphBuilder::check( return result; } -Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprConstantString* string, std::optional expectedType, bool forceSingleton) +Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprConstantString* string, std::optional expectedType, bool forceSingleton) { if (forceSingleton) return Inference{arena->addType(SingletonType{StringSingleton{std::string{string->value.data, string->value.size}}})}; @@ -1624,7 +1610,7 @@ Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprConstantSt return Inference{builtinTypes->stringType}; } -Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprConstantBool* boolExpr, std::optional expectedType, bool forceSingleton) +Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprConstantBool* boolExpr, std::optional expectedType, bool forceSingleton) { const TypeId singletonType = boolExpr->value ? builtinTypes->trueType : builtinTypes->falseType; if (forceSingleton) @@ -1649,7 +1635,7 @@ Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprConstantBo return Inference{builtinTypes->booleanType}; } -Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprLocal* local) +Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprLocal* local) { const RefinementKey* key = dfg->getRefinementKey(local); std::optional rvalueDef = dfg->getRValueDefForCompoundAssign(local); @@ -1675,10 +1661,10 @@ Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprLocal* loc return Inference{ty, refinementArena.proposition(key, builtinTypes->truthyType)}; } else - ice->ice("CGB: AstExprLocal came before its declaration?"); + ice->ice("CG: AstExprLocal came before its declaration?"); } -Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprGlobal* global) +Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprGlobal* global) { const RefinementKey* key = dfg->getRefinementKey(global); std::optional rvalueDef = dfg->getRValueDefForCompoundAssign(global); @@ -1704,7 +1690,7 @@ Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprGlobal* gl } } -Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprIndexName* indexName) +Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprIndexName* indexName) { TypeId obj = check(scope, indexName->expr).ty; TypeId result = arena->addType(BlockedType{}); @@ -1726,7 +1712,7 @@ Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprIndexName* return Inference{result}; } -Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprIndexExpr* indexExpr) +Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprIndexExpr* indexExpr) { TypeId obj = check(scope, indexExpr->expr).ty; TypeId indexType = check(scope, indexExpr->index).ty; @@ -1752,7 +1738,7 @@ Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprIndexExpr* return Inference{result}; } -Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprFunction* func, std::optional expectedType, bool generalize) +Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprFunction* func, std::optional expectedType, bool generalize) { Checkpoint startCheckpoint = checkpoint(this); FunctionSignature sig = checkFunctionSignature(scope, func, expectedType); @@ -1785,7 +1771,7 @@ Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprFunction* } } -Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprUnary* unary) +Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprUnary* unary) { auto [operandType, refinement] = check(scope, unary->expr); @@ -1826,7 +1812,7 @@ Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprUnary* una } } -Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprBinary* binary, std::optional expectedType) +Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprBinary* binary, std::optional expectedType) { auto [leftType, rightType, refinement] = checkBinary(scope, binary, expectedType); @@ -1990,7 +1976,7 @@ Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprBinary* bi } } -Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprIfElse* ifElse, std::optional expectedType) +Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprIfElse* ifElse, std::optional expectedType) { ScopePtr condScope = childScope(ifElse->condition, scope); RefinementId refinement = check(condScope, ifElse->condition).refinement; @@ -2006,13 +1992,13 @@ Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprIfElse* if return Inference{expectedType ? *expectedType : simplifyUnion(builtinTypes, arena, thenType, elseType).result}; } -Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprTypeAssertion* typeAssert) +Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprTypeAssertion* typeAssert) { check(scope, typeAssert->expr, std::nullopt); return Inference{resolveType(scope, typeAssert->annotation, /* inTypeArguments */ false)}; } -Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprInterpString* interpString) +Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprInterpString* interpString) { for (AstExpr* expr : interpString->expressions) check(scope, expr); @@ -2020,7 +2006,7 @@ Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprInterpStri return Inference{builtinTypes->stringType}; } -std::tuple ConstraintGraphBuilder::checkBinary( +std::tuple ConstraintGenerator::checkBinary( const ScopePtr& scope, AstExprBinary* binary, std::optional expectedType) { if (binary->op == AstExprBinary::And) @@ -2133,16 +2119,16 @@ std::tuple ConstraintGraphBuilder::checkBinary( } } -std::optional ConstraintGraphBuilder::checkLValue(const ScopePtr& scope, AstExpr* expr) +std::optional ConstraintGenerator::checkLValue(const ScopePtr& scope, AstExpr* expr, TypeId assignedTy) { if (auto local = expr->as()) - return checkLValue(scope, local); + return checkLValue(scope, local, assignedTy); else if (auto global = expr->as()) - return checkLValue(scope, global); + return checkLValue(scope, global, assignedTy); else if (auto indexName = expr->as()) - return checkLValue(scope, indexName); + return checkLValue(scope, indexName, assignedTy); else if (auto indexExpr = expr->as()) - return checkLValue(scope, indexExpr); + return checkLValue(scope, indexExpr, assignedTy); else if (auto error = expr->as()) { check(scope, error); @@ -2152,7 +2138,7 @@ std::optional ConstraintGraphBuilder::checkLValue(const ScopePtr& scope, ice->ice("checkLValue is inexhaustive"); } -std::optional ConstraintGraphBuilder::checkLValue(const ScopePtr& scope, AstExprLocal* local) +std::optional ConstraintGenerator::checkLValue(const ScopePtr& scope, AstExprLocal* local, TypeId assignedTy) { /* * The caller of this method uses the returned type to emit the proper @@ -2162,11 +2148,14 @@ std::optional ConstraintGraphBuilder::checkLValue(const ScopePtr& scope, * populated by symbols that have type annotations. * * If this local has an interesting type annotation, it is important that we - * return that. + * return that and constrain the assigned type. */ std::optional annotatedTy = scope->lookup(local->local); if (annotatedTy) + { + addConstraint(scope, local->location, SubtypeConstraint{assignedTy, *annotatedTy}); return annotatedTy; + } /* * As a safety measure, we'll assert that no type has yet been ascribed to @@ -2177,34 +2166,19 @@ std::optional ConstraintGraphBuilder::checkLValue(const ScopePtr& scope, return std::nullopt; } -std::optional ConstraintGraphBuilder::checkLValue(const ScopePtr& scope, AstExprGlobal* global) +std::optional ConstraintGenerator::checkLValue(const ScopePtr& scope, AstExprGlobal* global, TypeId assignedTy) { return scope->lookup(Symbol{global->name}); } -std::optional ConstraintGraphBuilder::checkLValue(const ScopePtr& scope, AstExprIndexName* indexName) +std::optional ConstraintGenerator::checkLValue(const ScopePtr& scope, AstExprIndexName* indexName, TypeId assignedTy) { - return updateProperty(scope, indexName); + return updateProperty(scope, indexName, assignedTy); } -std::optional ConstraintGraphBuilder::checkLValue(const ScopePtr& scope, AstExprIndexExpr* indexExpr) +std::optional ConstraintGenerator::checkLValue(const ScopePtr& scope, AstExprIndexExpr* indexExpr, TypeId assignedTy) { - return updateProperty(scope, indexExpr); -} - -static bool isIndexNameEquivalent(AstExpr* expr) -{ - if (expr->is()) - return true; - - AstExprIndexExpr* e = expr->as(); - if (e == nullptr) - return false; - - if (!e->index->is()) - return false; - - return true; + return updateProperty(scope, indexExpr, assignedTy); } /** @@ -2212,8 +2186,19 @@ static bool isIndexNameEquivalent(AstExpr* expr) * * If expr has the form name.a.b.c */ -TypeId ConstraintGraphBuilder::updateProperty(const ScopePtr& scope, AstExpr* expr) +TypeId ConstraintGenerator::updateProperty(const ScopePtr& scope, AstExpr* expr, TypeId assignedTy) { + // There are a bunch of cases where we realize that this is not the kind of + // assignment that potentially changes the shape of a table. When we + // encounter them, we call this to fall back and do the "usual thing." + auto fallback = [&]() { + TypeId resTy = check(scope, expr).ty; + addConstraint(scope, expr->location, SubtypeConstraint{assignedTy, resTy}); + return resTy; + }; + + LUAU_ASSERT(expr->is() || expr->is()); + if (auto indexExpr = expr->as(); indexExpr && !indexExpr->index->is()) { // An indexer is only interesting in an lvalue-ey way if it is at the @@ -2231,15 +2216,12 @@ TypeId ConstraintGraphBuilder::updateProperty(const ScopePtr& scope, AstExpr* ex TypeId resultType = arena->addType(BlockedType{}); TypeId subjectType = check(scope, indexExpr->expr).ty; TypeId indexType = check(scope, indexExpr->index).ty; - TypeId propType = arena->addType(BlockedType{}); - addConstraint(scope, expr->location, SetIndexerConstraint{resultType, subjectType, indexType, propType}); + addConstraint(scope, expr->location, SetIndexerConstraint{resultType, subjectType, indexType, assignedTy}); - module->astTypes[expr] = propType; + module->astTypes[expr] = assignedTy; - return propType; + return assignedTy; } - else if (!isIndexNameEquivalent(expr)) - return check(scope, expr).ty; Symbol sym; const Def* def = nullptr; @@ -2269,21 +2251,24 @@ TypeId ConstraintGraphBuilder::updateProperty(const ScopePtr& scope, AstExpr* ex } else if (auto indexExpr = e->as()) { - // We need to populate the type for the index value - check(scope, indexExpr->index); if (auto strIndex = indexExpr->index->as()) { + // We need to populate astTypes for the index value. + check(scope, indexExpr->index); + segments.push_back(std::string(strIndex->value.data, strIndex->value.size)); exprs.push_back(e); e = indexExpr->expr; } else { - return check(scope, expr).ty; + return fallback(); } } else - return check(scope, expr).ty; + { + return fallback(); + } } LUAU_ASSERT(!segments.empty()); @@ -2294,16 +2279,14 @@ TypeId ConstraintGraphBuilder::updateProperty(const ScopePtr& scope, AstExpr* ex LUAU_ASSERT(def); std::optional> lookupResult = scope->lookupEx(NotNull{def}); if (!lookupResult) - return check(scope, expr).ty; + return fallback(); const auto [subjectType, subjectScope] = *lookupResult; - TypeId propTy = freshType(scope); - std::vector segmentStrings(begin(segments), end(segments)); TypeId updatedType = arena->addType(BlockedType{}); - addConstraint(scope, expr->location, SetPropConstraint{updatedType, subjectType, std::move(segmentStrings), propTy}); + addConstraint(scope, expr->location, SetPropConstraint{updatedType, subjectType, std::move(segmentStrings), assignedTy}); TypeId prevSegmentTy = updatedType; for (size_t i = 0; i < segments.size(); ++i) @@ -2330,10 +2313,10 @@ TypeId ConstraintGraphBuilder::updateProperty(const ScopePtr& scope, AstExpr* ex } } - return propTy; + return assignedTy; } -void ConstraintGraphBuilder::updateLValueType(AstExpr* lvalue, TypeId ty) +void ConstraintGenerator::updateLValueType(AstExpr* lvalue, TypeId ty) { if (auto local = lvalue->as()) { @@ -2342,7 +2325,7 @@ void ConstraintGraphBuilder::updateLValueType(AstExpr* lvalue, TypeId ty) } } -Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprTable* expr, std::optional expectedType) +Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprTable* expr, std::optional expectedType) { const bool expectedTypeIsFree = expectedType && get(follow(*expectedType)); @@ -2462,7 +2445,7 @@ Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprTable* exp return Inference{ty}; } -ConstraintGraphBuilder::FunctionSignature ConstraintGraphBuilder::checkFunctionSignature( +ConstraintGenerator::FunctionSignature ConstraintGenerator::checkFunctionSignature( const ScopePtr& parent, AstExprFunction* fn, std::optional expectedType, std::optional originalName) { ScopePtr signatureScope = nullptr; @@ -2654,7 +2637,7 @@ ConstraintGraphBuilder::FunctionSignature ConstraintGraphBuilder::checkFunctionS }; } -void ConstraintGraphBuilder::checkFunctionBody(const ScopePtr& scope, AstExprFunction* fn) +void ConstraintGenerator::checkFunctionBody(const ScopePtr& scope, AstExprFunction* fn) { visitBlockWithoutChildScope(scope, fn->body); @@ -2662,12 +2645,12 @@ void ConstraintGraphBuilder::checkFunctionBody(const ScopePtr& scope, AstExprFun if (nullptr != getFallthrough(fn->body)) { - TypePackId empty = arena->addTypePack({}); // TODO we could have CGB retain one of these forever + TypePackId empty = arena->addTypePack({}); // TODO we could have CG retain one of these forever addConstraint(scope, fn->location, PackSubtypeConstraint{scope->returnType, empty}); } } -TypeId ConstraintGraphBuilder::resolveType(const ScopePtr& scope, AstType* ty, bool inTypeArguments, bool replaceErrorWithFresh) +TypeId ConstraintGenerator::resolveType(const ScopePtr& scope, AstType* ty, bool inTypeArguments, bool replaceErrorWithFresh) { TypeId result = nullptr; @@ -2895,7 +2878,7 @@ TypeId ConstraintGraphBuilder::resolveType(const ScopePtr& scope, AstType* ty, b return result; } -TypePackId ConstraintGraphBuilder::resolveTypePack(const ScopePtr& scope, AstTypePack* tp, bool inTypeArgument, bool replaceErrorWithFresh) +TypePackId ConstraintGenerator::resolveTypePack(const ScopePtr& scope, AstTypePack* tp, bool inTypeArgument, bool replaceErrorWithFresh) { TypePackId result; if (auto expl = tp->as()) @@ -2929,7 +2912,7 @@ TypePackId ConstraintGraphBuilder::resolveTypePack(const ScopePtr& scope, AstTyp return result; } -TypePackId ConstraintGraphBuilder::resolveTypePack(const ScopePtr& scope, const AstTypeList& list, bool inTypeArguments, bool replaceErrorWithFresh) +TypePackId ConstraintGenerator::resolveTypePack(const ScopePtr& scope, const AstTypeList& list, bool inTypeArguments, bool replaceErrorWithFresh) { std::vector head; @@ -2947,7 +2930,7 @@ TypePackId ConstraintGraphBuilder::resolveTypePack(const ScopePtr& scope, const return arena->addTypePack(TypePack{head, tail}); } -std::vector> ConstraintGraphBuilder::createGenerics( +std::vector> ConstraintGenerator::createGenerics( const ScopePtr& scope, AstArray generics, bool useCache, bool addTypes) { std::vector> result; @@ -2977,7 +2960,7 @@ std::vector> ConstraintGraphBuilder::crea return result; } -std::vector> ConstraintGraphBuilder::createGenericPacks( +std::vector> ConstraintGenerator::createGenericPacks( const ScopePtr& scope, AstArray generics, bool useCache, bool addTypes) { std::vector> result; @@ -3008,7 +2991,7 @@ std::vector> ConstraintGraphBuilder:: return result; } -Inference ConstraintGraphBuilder::flattenPack(const ScopePtr& scope, Location location, InferencePack pack) +Inference ConstraintGenerator::flattenPack(const ScopePtr& scope, Location location, InferencePack pack) { const auto& [tp, refinements] = pack; RefinementId refinement = nullptr; @@ -3025,7 +3008,7 @@ Inference ConstraintGraphBuilder::flattenPack(const ScopePtr& scope, Location lo return Inference{typeResult, refinement}; } -void ConstraintGraphBuilder::reportError(Location location, TypeErrorData err) +void ConstraintGenerator::reportError(Location location, TypeErrorData err) { errors.push_back(TypeError{location, module->name, std::move(err)}); @@ -3033,7 +3016,7 @@ void ConstraintGraphBuilder::reportError(Location location, TypeErrorData err) logger->captureGenerationError(errors.back()); } -void ConstraintGraphBuilder::reportCodeTooComplex(Location location) +void ConstraintGenerator::reportCodeTooComplex(Location location) { errors.push_back(TypeError{location, module->name, CodeTooComplex{}}); @@ -3069,7 +3052,7 @@ struct GlobalPrepopulator : AstVisitor } }; -void ConstraintGraphBuilder::prepopulateGlobalScope(const ScopePtr& globalScope, AstStatBlock* program) +void ConstraintGenerator::prepopulateGlobalScope(const ScopePtr& globalScope, AstStatBlock* program) { GlobalPrepopulator gp{NotNull{globalScope.get()}, arena, dfg}; @@ -3079,7 +3062,7 @@ void ConstraintGraphBuilder::prepopulateGlobalScope(const ScopePtr& globalScope, program->visit(&gp); } -void ConstraintGraphBuilder::fillInInferredBindings(const ScopePtr& globalScope, AstStatBlock* block) +void ConstraintGenerator::fillInInferredBindings(const ScopePtr& globalScope, AstStatBlock* block) { for (const auto& [symbol, p] : inferredBindings) { @@ -3094,7 +3077,7 @@ void ConstraintGraphBuilder::fillInInferredBindings(const ScopePtr& globalScope, } } -std::vector> ConstraintGraphBuilder::getExpectedCallTypesForFunctionOverloads(const TypeId fnType) +std::vector> ConstraintGenerator::getExpectedCallTypesForFunctionOverloads(const TypeId fnType) { std::vector funTys; if (auto it = get(follow(fnType))) diff --git a/Analysis/src/DataFlowGraph.cpp b/Analysis/src/DataFlowGraph.cpp index e3933574..3f78f3a6 100644 --- a/Analysis/src/DataFlowGraph.cpp +++ b/Analysis/src/DataFlowGraph.cpp @@ -34,7 +34,7 @@ DefId DataFlowGraph::getDef(const AstExpr* expr) const std::optional DataFlowGraph::getRValueDefForCompoundAssign(const AstExpr* expr) const { - auto def = compoundAssignBreadcrumbs.find(expr); + auto def = compoundAssignDefs.find(expr); return def ? std::optional(*def) : std::nullopt; } @@ -628,11 +628,11 @@ void DataFlowGraphBuilder::visitLValue(DfgScope* scope, AstExpr* e, DefId incomi void DataFlowGraphBuilder::visitLValue(DfgScope* scope, AstExprLocal* l, DefId incomingDef, bool isCompoundAssignment) { - // We need to keep the previous breadcrumb around for a compound assignment. + // We need to keep the previous def around for a compound assignment. if (isCompoundAssignment) { if (auto def = scope->lookup(l->local)) - graph.compoundAssignBreadcrumbs[l] = *def; + graph.compoundAssignDefs[l] = *def; } // In order to avoid alias tracking, we need to clip the reference to the parent def. @@ -643,11 +643,11 @@ void DataFlowGraphBuilder::visitLValue(DfgScope* scope, AstExprLocal* l, DefId i void DataFlowGraphBuilder::visitLValue(DfgScope* scope, AstExprGlobal* g, DefId incomingDef, bool isCompoundAssignment) { - // We need to keep the previous breadcrumb around for a compound assignment. + // We need to keep the previous def around for a compound assignment. if (isCompoundAssignment) { if (auto def = scope->lookup(g->name)) - graph.compoundAssignBreadcrumbs[g] = *def; + graph.compoundAssignDefs[g] = *def; } // In order to avoid alias tracking, we need to clip the reference to the parent def. diff --git a/Analysis/src/Frontend.cpp b/Analysis/src/Frontend.cpp index 6cbc19fa..feea40c4 100644 --- a/Analysis/src/Frontend.cpp +++ b/Analysis/src/Frontend.cpp @@ -5,7 +5,7 @@ #include "Luau/Clone.h" #include "Luau/Common.h" #include "Luau/Config.h" -#include "Luau/ConstraintGraphBuilder.h" +#include "Luau/ConstraintGenerator.h" #include "Luau/ConstraintSolver.h" #include "Luau/DataFlowGraph.h" #include "Luau/DcrLogger.h" @@ -1255,13 +1255,13 @@ ModulePtr check(const SourceModule& sourceModule, Mode mode, const std::vectorinternalTypes, builtinTypes, NotNull{&unifierState}}; - ConstraintGraphBuilder cgb{result, NotNull{&normalizer}, moduleResolver, builtinTypes, iceHandler, parentScope, std::move(prepareModuleScope), + ConstraintGenerator cg{result, NotNull{&normalizer}, moduleResolver, builtinTypes, iceHandler, parentScope, std::move(prepareModuleScope), logger.get(), NotNull{&dfg}, requireCycles}; - cgb.visitModuleRoot(sourceModule.root); - result->errors = std::move(cgb.errors); + cg.visitModuleRoot(sourceModule.root); + result->errors = std::move(cg.errors); - ConstraintSolver cs{NotNull{&normalizer}, NotNull(cgb.rootScope), borrowConstraints(cgb.constraints), result->humanReadableName, moduleResolver, + ConstraintSolver cs{NotNull{&normalizer}, NotNull(cg.rootScope), borrowConstraints(cg.constraints), result->humanReadableName, moduleResolver, requireCycles, logger.get(), limits}; if (options.randomizeConstraintResolutionSeed) @@ -1283,7 +1283,7 @@ ModulePtr check(const SourceModule& sourceModule, Mode mode, const std::vectorerrors.emplace_back(std::move(e)); - result->scopes = std::move(cgb.scopes); + result->scopes = std::move(cg.scopes); result->type = sourceModule.type; result->clonePublicInterface(builtinTypes, *iceHandler); diff --git a/Analysis/src/Linter.cpp b/Analysis/src/Linter.cpp index e957eee7..4aef48c3 100644 --- a/Analysis/src/Linter.cpp +++ b/Analysis/src/Linter.cpp @@ -14,9 +14,6 @@ LUAU_FASTINTVARIABLE(LuauSuggestionDistance, 4) -LUAU_FASTFLAGVARIABLE(LuauLintDeprecatedFenv, false) -LUAU_FASTFLAGVARIABLE(LuauLintTableIndexer, false) - namespace Luau { @@ -2093,7 +2090,7 @@ private: // getfenv/setfenv are deprecated, however they are still used in some test frameworks and don't have a great general replacement // for now we warn about the deprecation only when they are used with a numeric first argument; this produces fewer warnings and makes use // of getfenv/setfenv a little more localized - if (FFlag::LuauLintDeprecatedFenv && !node->self && node->args.size >= 1) + if (!node->self && node->args.size >= 1) { if (AstExprGlobal* fenv = node->func->as(); fenv && (fenv->name == "getfenv" || fenv->name == "setfenv")) { @@ -2185,7 +2182,7 @@ private: bool visit(AstExprUnary* node) override { - if (FFlag::LuauLintTableIndexer && node->op == AstExprUnary::Len) + if (node->op == AstExprUnary::Len) checkIndexer(node, node->expr, "#"); return true; @@ -2195,7 +2192,7 @@ private: { if (AstExprGlobal* func = node->func->as()) { - if (FFlag::LuauLintTableIndexer && func->name == "ipairs" && node->args.size == 1) + if (func->name == "ipairs" && node->args.size == 1) checkIndexer(node, node->args.data[0], "ipairs"); } else if (AstExprIndexName* func = node->func->as()) @@ -2209,8 +2206,6 @@ private: void checkIndexer(AstExpr* node, AstExpr* expr, const char* op) { - LUAU_ASSERT(FFlag::LuauLintTableIndexer); - std::optional ty = context->getType(expr); if (!ty) return; @@ -2653,13 +2648,17 @@ private: case ConstantNumberParseResult::Ok: case ConstantNumberParseResult::Malformed: break; + case ConstantNumberParseResult::Imprecise: + emitWarning(*context, LintWarning::Code_IntegerParsing, node->location, + "Number literal exceeded available precision and was truncated to closest representable number"); + break; case ConstantNumberParseResult::BinOverflow: emitWarning(*context, LintWarning::Code_IntegerParsing, node->location, - "Binary number literal exceeded available precision and has been truncated to 2^64"); + "Binary number literal exceeded available precision and was truncated to 2^64"); break; case ConstantNumberParseResult::HexOverflow: emitWarning(*context, LintWarning::Code_IntegerParsing, node->location, - "Hexadecimal number literal exceeded available precision and has been truncated to 2^64"); + "Hexadecimal number literal exceeded available precision and was truncated to 2^64"); break; } diff --git a/Analysis/src/Module.cpp b/Analysis/src/Module.cpp index 580f59f3..d50719a9 100644 --- a/Analysis/src/Module.cpp +++ b/Analysis/src/Module.cpp @@ -3,7 +3,7 @@ #include "Luau/Clone.h" #include "Luau/Common.h" -#include "Luau/ConstraintGraphBuilder.h" +#include "Luau/ConstraintGenerator.h" #include "Luau/Normalize.h" #include "Luau/RecursionCounter.h" #include "Luau/Scope.h" diff --git a/Analysis/src/Normalize.cpp b/Analysis/src/Normalize.cpp index 52bbc5d9..c21f7f32 100644 --- a/Analysis/src/Normalize.cpp +++ b/Analysis/src/Normalize.cpp @@ -8,7 +8,9 @@ #include "Luau/Clone.h" #include "Luau/Common.h" #include "Luau/RecursionCounter.h" +#include "Luau/Subtyping.h" #include "Luau/Type.h" +#include "Luau/TypeFwd.h" #include "Luau/Unifier.h" LUAU_FASTFLAGVARIABLE(DebugLuauCheckNormalizeInvariant, false) @@ -19,6 +21,7 @@ LUAU_FASTINTVARIABLE(LuauNormalizeCacheLimit, 100000); LUAU_FASTFLAGVARIABLE(LuauNormalizeCyclicUnions, false); LUAU_FASTFLAG(LuauTransitiveSubtyping) LUAU_FASTFLAG(DebugLuauReadWriteProperties) +LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution) namespace Luau { @@ -32,9 +35,14 @@ TypeIds::TypeIds(std::initializer_list tys) void TypeIds::insert(TypeId ty) { ty = follow(ty); - auto [_, fresh] = types.insert(ty); - if (fresh) + + // get a reference to the slot for `ty` in `types` + bool& entry = types[ty]; + + // if `ty` is fresh, we can set it to `true`, add it to the order and hash and be done. + if (!entry) { + entry = true; order.push_back(ty); hash ^= std::hash{}(ty); } @@ -75,25 +83,26 @@ TypeIds::const_iterator TypeIds::end() const TypeIds::iterator TypeIds::erase(TypeIds::const_iterator it) { TypeId ty = *it; - types.erase(ty); + types[ty] = false; hash ^= std::hash{}(ty); return order.erase(it); } size_t TypeIds::size() const { - return types.size(); + return order.size(); } bool TypeIds::empty() const { - return types.empty(); + return order.empty(); } size_t TypeIds::count(TypeId ty) const { ty = follow(ty); - return types.count(ty); + const bool* val = types.find(ty); + return (val && *val) ? 1 : 0; } void TypeIds::retain(const TypeIds& there) @@ -122,7 +131,29 @@ bool TypeIds::isNever() const bool TypeIds::operator==(const TypeIds& there) const { - return hash == there.hash && types == there.types; + // we can early return if the hashes don't match. + if (hash != there.hash) + return false; + + // we have to check equality of the sets themselves if not. + + // if the sets are unequal sizes, then they cannot possibly be equal. + // it is important to use `order` here and not `types` since the mappings + // may have different sizes since removal is not possible, and so erase + // simply writes `false` into the map. + if (order.size() != there.order.size()) + return false; + + // otherwise, we'll need to check that every element we have here is in `there`. + for (auto ty : order) + { + // if it's not, we'll return `false` + if (there.count(ty) == 0) + return false; + } + + // otherwise, we've proven the two equal! + return true; } NormalizedStringType::NormalizedStringType() {} @@ -240,6 +271,42 @@ NormalizedType::NormalizedType(NotNull builtinTypes) { } +bool NormalizedType::isUnknown() const +{ + if (get(tops)) + return true; + + // Otherwise, we can still be unknown! + bool hasAllPrimitives = isPrim(booleans, PrimitiveType::Boolean) && isPrim(nils, PrimitiveType::NilType) && isNumber(numbers) && + strings.isString() && isPrim(threads, PrimitiveType::Thread) && isThread(threads); + + // Check is class + bool isTopClass = false; + for (auto [t, disj] : classes.classes) + { + if (auto ct = get(t)) + { + if (ct->name == "class" && disj.empty()) + { + isTopClass = true; + break; + } + } + } + // Check is table + bool isTopTable = false; + for (auto t : tables) + { + if (isPrim(t, PrimitiveType::Table)) + { + isTopTable = true; + break; + } + } + // any = unknown or error ==> we need to make sure we have all the unknown components, but not errors + return get(errors) && hasAllPrimitives && isTopClass && isTopTable && functions.isTop; +} + bool NormalizedType::isExactlyNumber() const { return hasNumbers() && !hasTops() && !hasBooleans() && !hasClasses() && !hasErrors() && !hasNils() && !hasStrings() && !hasThreads() && @@ -647,8 +714,7 @@ static bool areNormalizedClasses(const NormalizedClassType& tys) static bool isPlainTyvar(TypeId ty) { - return (get(ty) || get(ty) || get(ty) || - get(ty) || get(ty)); + return (get(ty) || get(ty) || get(ty) || get(ty) || get(ty)); } static bool isNormalizedTyvar(const NormalizedTyvars& tyvars) @@ -711,6 +777,11 @@ const NormalizedType* Normalizer::normalize(TypeId ty) std::unordered_set seenSetTypes; if (!unionNormalWithTy(norm, ty, seenSetTypes)) return nullptr; + if (norm.isUnknown()) + { + clearNormal(norm); + norm.tops = builtinTypes->unknownType; + } std::unique_ptr uniq = std::make_unique(std::move(norm)); const NormalizedType* result = uniq.get(); cachedNormals[ty] = std::move(uniq); @@ -1520,8 +1591,8 @@ bool Normalizer::unionNormalWithTy(NormalizedType& here, TypeId there, std::unor } else if (FFlag::LuauTransitiveSubtyping && get(here.tops)) return true; - else if (get(there) || get(there) || get(there) || - get(there) || get(there)) + else if (get(there) || get(there) || get(there) || get(there) || + get(there)) { if (tyvarIndex(there) <= ignoreSmallerTyvars) return true; @@ -2661,8 +2732,8 @@ bool Normalizer::intersectNormalWithTy(NormalizedType& here, TypeId there, std:: return false; return true; } - else if (get(there) || get(there) || get(there) || - get(there) || get(there)) + else if (get(there) || get(there) || get(there) || get(there) || + get(there)) { NormalizedType thereNorm{builtinTypes}; NormalizedType topNorm{builtinTypes}; @@ -2915,32 +2986,58 @@ TypeId Normalizer::typeFromNormal(const NormalizedType& norm) bool isSubtype(TypeId subTy, TypeId superTy, NotNull scope, NotNull builtinTypes, InternalErrorReporter& ice) { - if (!FFlag::LuauTransitiveSubtyping) + if (!FFlag::LuauTransitiveSubtyping && !FFlag::DebugLuauDeferredConstraintResolution) return isConsistentSubtype(subTy, superTy, scope, builtinTypes, ice); + UnifierSharedState sharedState{&ice}; TypeArena arena; Normalizer normalizer{&arena, builtinTypes, NotNull{&sharedState}}; - Unifier u{NotNull{&normalizer}, scope, Location{}, Covariant}; - u.tryUnify(subTy, superTy); - return !u.failure; + // Subtyping under DCR is not implemented using unification! + if (FFlag::DebugLuauDeferredConstraintResolution) + { + Subtyping subtyping{builtinTypes, NotNull{&arena}, NotNull{&normalizer}, NotNull{&ice}, scope}; + + return subtyping.isSubtype(subTy, superTy).isSubtype; + } + else + { + Unifier u{NotNull{&normalizer}, scope, Location{}, Covariant}; + + u.tryUnify(subTy, superTy); + return !u.failure; + } } bool isSubtype(TypePackId subPack, TypePackId superPack, NotNull scope, NotNull builtinTypes, InternalErrorReporter& ice) { - if (!FFlag::LuauTransitiveSubtyping) + if (!FFlag::LuauTransitiveSubtyping && !FFlag::DebugLuauDeferredConstraintResolution) return isConsistentSubtype(subPack, superPack, scope, builtinTypes, ice); + UnifierSharedState sharedState{&ice}; TypeArena arena; Normalizer normalizer{&arena, builtinTypes, NotNull{&sharedState}}; - Unifier u{NotNull{&normalizer}, scope, Location{}, Covariant}; - u.tryUnify(subPack, superPack); - return !u.failure; + // Subtyping under DCR is not implemented using unification! + if (FFlag::DebugLuauDeferredConstraintResolution) + { + Subtyping subtyping{builtinTypes, NotNull{&arena}, NotNull{&normalizer}, NotNull{&ice}, scope}; + + return subtyping.isSubtype(subPack, superPack).isSubtype; + } + else + { + Unifier u{NotNull{&normalizer}, scope, Location{}, Covariant}; + + u.tryUnify(subPack, superPack); + return !u.failure; + } } bool isConsistentSubtype(TypeId subTy, TypeId superTy, NotNull scope, NotNull builtinTypes, InternalErrorReporter& ice) { + LUAU_ASSERT(!FFlag::DebugLuauDeferredConstraintResolution); + UnifierSharedState sharedState{&ice}; TypeArena arena; Normalizer normalizer{&arena, builtinTypes, NotNull{&sharedState}}; @@ -2954,6 +3051,8 @@ bool isConsistentSubtype(TypeId subTy, TypeId superTy, NotNull scope, Not bool isConsistentSubtype( TypePackId subPack, TypePackId superPack, NotNull scope, NotNull builtinTypes, InternalErrorReporter& ice) { + LUAU_ASSERT(!FFlag::DebugLuauDeferredConstraintResolution); + UnifierSharedState sharedState{&ice}; TypeArena arena; Normalizer normalizer{&arena, builtinTypes, NotNull{&sharedState}}; diff --git a/Analysis/src/Subtyping.cpp b/Analysis/src/Subtyping.cpp index e386bf7b..6e386e68 100644 --- a/Analysis/src/Subtyping.cpp +++ b/Analysis/src/Subtyping.cpp @@ -321,14 +321,26 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypeId sub if (auto subUnion = get(subTy)) result = isCovariantWith(env, subUnion, superTy); else if (auto superUnion = get(superTy)) + { result = isCovariantWith(env, subTy, superUnion); + if (!result.isSubtype && !result.isErrorSuppressing && !result.normalizationTooComplex) + { + SubtypingResult semantic = isCovariantWith(env, normalizer->normalize(subTy), normalizer->normalize(superTy)); + if (semantic.isSubtype) + result = semantic; + } + } else if (auto superIntersection = get(superTy)) result = isCovariantWith(env, subTy, superIntersection); else if (auto subIntersection = get(subTy)) { result = isCovariantWith(env, subIntersection, superTy); if (!result.isSubtype && !result.isErrorSuppressing && !result.normalizationTooComplex) - result = isCovariantWith(env, normalizer->normalize(subTy), normalizer->normalize(superTy)); + { + SubtypingResult semantic = isCovariantWith(env, normalizer->normalize(subTy), normalizer->normalize(superTy)); + if (semantic.isSubtype) + result = semantic; + } } else if (get(superTy)) result = {true}; diff --git a/Analysis/src/TypeChecker2.cpp b/Analysis/src/TypeChecker2.cpp index bf8e362d..bd637453 100644 --- a/Analysis/src/TypeChecker2.cpp +++ b/Analysis/src/TypeChecker2.cpp @@ -2413,6 +2413,31 @@ struct TypeChecker2 } } + void explainError(TypeId subTy, TypeId superTy, Location location, const SubtypingResult& r) + { + if (!r.reasoning) + return reportError(TypeMismatch{superTy, subTy}, location); + + std::optional subLeaf = traverse(subTy, r.reasoning->subPath, builtinTypes); + std::optional superLeaf = traverse(superTy, r.reasoning->superPath, builtinTypes); + + if (!subLeaf || !superLeaf) + ice->ice("Subtyping test returned a reasoning with an invalid path", location); + + if (!get2(*subLeaf, *superLeaf) && !get2(*subLeaf, *superLeaf)) + ice->ice("Subtyping test returned a reasoning where one path ends at a type and the other ends at a pack.", location); + + std::string reason; + + 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) + ")"; + + reportError(TypeMismatch{superTy, subTy, reason}, location); + } + bool testIsSubtype(TypeId subTy, TypeId superTy, Location location) { SubtypingResult r = subtyping->isSubtype(subTy, superTy); @@ -2421,27 +2446,7 @@ struct TypeChecker2 reportError(NormalizationTooComplex{}, location); if (!r.isSubtype && !r.isErrorSuppressing) - { - if (r.reasoning) - { - std::optional subLeaf = traverse(subTy, r.reasoning->subPath, builtinTypes); - std::optional superLeaf = traverse(superTy, r.reasoning->superPath, builtinTypes); - - if (!subLeaf || !superLeaf) - ice->ice("Subtyping test returned a reasoning with an invalid path", location); - - if (!get2(*subLeaf, *superLeaf) && !get2(*subLeaf, *superLeaf)) - ice->ice("Subtyping test returned a reasoning where one path ends at a type and the other ends at a pack.", location); - - std::string reason = "type " + toString(subTy) + toString(r.reasoning->subPath) + " (" + toString(*subLeaf) + - ") is not a subtype of " + toString(superTy) + toString(r.reasoning->superPath) + " (" + toString(*superLeaf) + - ")"; - - reportError(TypeMismatch{superTy, subTy, reason}, location); - } - else - reportError(TypeMismatch{superTy, subTy}, location); - } + explainError(subTy, superTy, location, r); return r.isSubtype; } diff --git a/Analysis/src/TypeInfer.cpp b/Analysis/src/TypeInfer.cpp index 1ff8b30b..1cc32bdc 100644 --- a/Analysis/src/TypeInfer.cpp +++ b/Analysis/src/TypeInfer.cpp @@ -35,11 +35,9 @@ LUAU_FASTFLAG(LuauKnowsTheDataModel3) LUAU_FASTFLAGVARIABLE(DebugLuauFreezeDuringUnification, false) LUAU_FASTFLAGVARIABLE(DebugLuauSharedSelf, false) LUAU_FASTFLAG(LuauInstantiateInSubtyping) -LUAU_FASTFLAGVARIABLE(LuauAllowIndexClassParameters, false) LUAU_FASTFLAG(LuauOccursIsntAlwaysFailure) LUAU_FASTFLAGVARIABLE(LuauTinyControlFlowAnalysis, false) LUAU_FASTFLAGVARIABLE(LuauLoopControlFlowAnalysis, false) -LUAU_FASTFLAGVARIABLE(LuauVariadicOverloadFix, false) LUAU_FASTFLAGVARIABLE(LuauAlwaysCommitInferencesOfFunctionCalls, false) LUAU_FASTFLAG(LuauParseDeclareClassIndexer) LUAU_FASTFLAG(LuauFloorDivision); @@ -3412,15 +3410,12 @@ TypeId TypeChecker::checkLValueBinding(const ScopePtr& scope, const AstExprIndex } } - if (FFlag::LuauAllowIndexClassParameters) + if (const ClassType* exprClass = get(exprType)) { - if (const ClassType* exprClass = get(exprType)) - { - if (isNonstrictMode()) - return unknownType; - reportError(TypeError{expr.location, DynamicPropertyLookupOnClassesUnsafe{exprType}}); - return errorRecoveryType(scope); - } + if (isNonstrictMode()) + return unknownType; + reportError(TypeError{expr.location, DynamicPropertyLookupOnClassesUnsafe{exprType}}); + return errorRecoveryType(scope); } } @@ -4026,13 +4021,9 @@ void TypeChecker::checkArgumentList(const ScopePtr& scope, const AstExpr& funNam if (argIndex < argLocations.size()) location = argLocations[argIndex]; - if (FFlag::LuauVariadicOverloadFix) - { - state.location = location; - state.tryUnify(*argIter, vtp->ty); - } - else - unify(*argIter, vtp->ty, scope, location); + state.location = location; + state.tryUnify(*argIter, vtp->ty); + ++argIter; ++argIndex; } diff --git a/Analysis/src/Unifier.cpp b/Analysis/src/Unifier.cpp index c371e81e..0940ea92 100644 --- a/Analysis/src/Unifier.cpp +++ b/Analysis/src/Unifier.cpp @@ -18,7 +18,6 @@ LUAU_FASTINT(LuauTypeInferTypePackLoopLimit) LUAU_FASTFLAG(LuauErrorRecoveryType) LUAU_FASTFLAGVARIABLE(LuauInstantiateInSubtyping, false) -LUAU_FASTFLAGVARIABLE(LuauMaintainScopesInUnifier, false) LUAU_FASTFLAGVARIABLE(LuauTransitiveSubtyping, false) LUAU_FASTFLAGVARIABLE(LuauOccursIsntAlwaysFailure, false) LUAU_FASTFLAG(LuauAlwaysCommitInferencesOfFunctionCalls) @@ -1514,7 +1513,7 @@ struct WeirdIter auto freePack = log.getMutable(packId); level = freePack->level; - if (FFlag::LuauMaintainScopesInUnifier && freePack->scope != nullptr) + if (freePack->scope != nullptr) scope = freePack->scope; log.replace(packId, BoundTypePack(newTail)); packId = newTail; @@ -1679,11 +1678,8 @@ void Unifier::tryUnify_(TypePackId subTp, TypePackId superTp, bool isFunctionCal auto superIter = WeirdIter(superTp, log); auto subIter = WeirdIter(subTp, log); - if (FFlag::LuauMaintainScopesInUnifier) - { - superIter.scope = scope.get(); - subIter.scope = scope.get(); - } + superIter.scope = scope.get(); + subIter.scope = scope.get(); auto mkFreshType = [this](Scope* scope, TypeLevel level) { if (FFlag::DebugLuauDeferredConstraintResolution) diff --git a/Ast/include/Luau/Ast.h b/Ast/include/Luau/Ast.h index a3908a56..ad5592f5 100644 --- a/Ast/include/Luau/Ast.h +++ b/Ast/include/Luau/Ast.h @@ -249,6 +249,7 @@ public: enum class ConstantNumberParseResult { Ok, + Imprecise, Malformed, BinOverflow, HexOverflow, diff --git a/Ast/include/Luau/Location.h b/Ast/include/Luau/Location.h index 041a2c63..3fc8921a 100644 --- a/Ast/include/Luau/Location.h +++ b/Ast/include/Luau/Location.h @@ -1,7 +1,6 @@ // This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details #pragma once -#include namespace Luau { @@ -9,7 +8,11 @@ struct Position { unsigned int line, column; - Position(unsigned int line, unsigned int column); + Position(unsigned int line, unsigned int column) + : line(line) + , column(column) + { + } bool operator==(const Position& rhs) const; bool operator!=(const Position& rhs) const; @@ -25,10 +28,29 @@ struct Location { Position begin, end; - Location(); - Location(const Position& begin, const Position& end); - Location(const Position& begin, unsigned int length); - Location(const Location& begin, const Location& end); + Location() + : begin(0, 0) + , end(0, 0) + { + } + + Location(const Position& begin, const Position& end) + : begin(begin) + , end(end) + { + } + + Location(const Position& begin, unsigned int length) + : begin(begin) + , end(begin.line, begin.column + length) + { + } + + Location(const Location& begin, const Location& end) + : begin(begin.begin) + , end(end.end) + { + } bool operator==(const Location& rhs) const; bool operator!=(const Location& rhs) const; diff --git a/Ast/src/Location.cpp b/Ast/src/Location.cpp index 40f8e23e..c2c66d9f 100644 --- a/Ast/src/Location.cpp +++ b/Ast/src/Location.cpp @@ -1,16 +1,9 @@ // This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details #include "Luau/Location.h" -#include namespace Luau { -Position::Position(unsigned int line, unsigned int column) - : line(line) - , column(column) -{ -} - bool Position::operator==(const Position& rhs) const { return this->column == rhs.column && this->line == rhs.line; @@ -61,30 +54,6 @@ void Position::shift(const Position& start, const Position& oldEnd, const Positi } } -Location::Location() - : begin(0, 0) - , end(0, 0) -{ -} - -Location::Location(const Position& begin, const Position& end) - : begin(begin) - , end(end) -{ -} - -Location::Location(const Position& begin, unsigned int length) - : begin(begin) - , end(begin.line, begin.column + length) -{ -} - -Location::Location(const Location& begin, const Location& end) - : begin(begin.begin) - , end(end.end) -{ -} - bool Location::operator==(const Location& rhs) const { return this->begin == rhs.begin && this->end == rhs.end; diff --git a/Ast/src/Parser.cpp b/Ast/src/Parser.cpp index a9747143..3871ea62 100644 --- a/Ast/src/Parser.cpp +++ b/Ast/src/Parser.cpp @@ -24,6 +24,8 @@ LUAU_FASTFLAG(LuauCheckedFunctionSyntax) LUAU_FASTFLAGVARIABLE(LuauBetterTypeUnionLimits, false) LUAU_FASTFLAGVARIABLE(LuauBetterTypeRecLimits, false) +LUAU_FASTFLAGVARIABLE(LuauParseImpreciseNumber, false) + namespace Luau { @@ -2187,6 +2189,12 @@ static ConstantNumberParseResult parseInteger(double& result, const char* data, return base == 2 ? ConstantNumberParseResult::BinOverflow : ConstantNumberParseResult::HexOverflow; } + if (FFlag::LuauParseImpreciseNumber) + { + if (value >= (1ull << 53) && static_cast(result) != value) + return ConstantNumberParseResult::Imprecise; + } + return ConstantNumberParseResult::Ok; } @@ -2203,8 +2211,32 @@ static ConstantNumberParseResult parseDouble(double& result, const char* data) char* end = nullptr; double value = strtod(data, &end); - result = value; - return *end == 0 ? ConstantNumberParseResult::Ok : ConstantNumberParseResult::Malformed; + if (FFlag::LuauParseImpreciseNumber) + { + // trailing non-numeric characters + if (*end != 0) + return ConstantNumberParseResult::Malformed; + + result = value; + + // for linting, we detect integer constants that are parsed imprecisely + // since the check is expensive we only perform it when the number is larger than the precise integer range + if (value >= double(1ull << 53) && strspn(data, "0123456789") == strlen(data)) + { + char repr[512]; + snprintf(repr, sizeof(repr), "%.0f", value); + + if (strcmp(repr, data) != 0) + return ConstantNumberParseResult::Imprecise; + } + + return ConstantNumberParseResult::Ok; + } + else + { + result = value; + return *end == 0 ? ConstantNumberParseResult::Ok : ConstantNumberParseResult::Malformed; + } } // simpleexp -> NUMBER | STRING | NIL | true | false | ... | constructor | FUNCTION body | primaryexp diff --git a/CLI/Compile.cpp b/CLI/Compile.cpp index 4f6b54b6..bc1d5c60 100644 --- a/CLI/Compile.cpp +++ b/CLI/Compile.cpp @@ -120,6 +120,7 @@ struct CompileStats { size_t lines; size_t bytecode; + size_t bytecodeInstructionCount; size_t codegen; double readTime; @@ -136,6 +137,7 @@ struct CompileStats fprintf(fp, "{\ \"lines\": %zu, \ \"bytecode\": %zu, \ +\"bytecodeInstructionCount\": %zu, \ \"codegen\": %zu, \ \"readTime\": %f, \ \"miscTime\": %f, \ @@ -153,16 +155,22 @@ struct CompileStats \"maxBlockInstructions\": %u, \ \"regAllocErrors\": %d, \ \"loweringErrors\": %d\ +}, \ +\"blockLinearizationStats\": {\ +\"constPropInstructionCount\": %u, \ +\"timeSeconds\": %f\ }}", - lines, bytecode, codegen, readTime, miscTime, parseTime, compileTime, codegenTime, lowerStats.totalFunctions, lowerStats.skippedFunctions, - lowerStats.spillsToSlot, lowerStats.spillsToRestore, lowerStats.maxSpillSlotsUsed, lowerStats.blocksPreOpt, lowerStats.blocksPostOpt, - lowerStats.maxBlockInstructions, lowerStats.regAllocErrors, lowerStats.loweringErrors); + lines, bytecode, bytecodeInstructionCount, codegen, readTime, miscTime, parseTime, compileTime, codegenTime, lowerStats.totalFunctions, + lowerStats.skippedFunctions, lowerStats.spillsToSlot, lowerStats.spillsToRestore, lowerStats.maxSpillSlotsUsed, lowerStats.blocksPreOpt, + lowerStats.blocksPostOpt, lowerStats.maxBlockInstructions, lowerStats.regAllocErrors, lowerStats.loweringErrors, + lowerStats.blockLinearizationStats.constPropInstructionCount, lowerStats.blockLinearizationStats.timeSeconds); } CompileStats& operator+=(const CompileStats& that) { this->lines += that.lines; this->bytecode += that.bytecode; + this->bytecodeInstructionCount += that.bytecodeInstructionCount; this->codegen += that.codegen; this->readTime += that.readTime; this->miscTime += that.miscTime; @@ -257,6 +265,7 @@ static bool compileFile(const char* name, CompileFormat format, Luau::CodeGen::A Luau::compileOrThrow(bcb, result, names, copts()); stats.bytecode += bcb.getBytecode().size(); + stats.bytecodeInstructionCount = bcb.getTotalInstructionCount(); stats.compileTime += recordDeltaTime(currts); switch (format) @@ -321,6 +330,30 @@ static int assertionHandler(const char* expr, const char* file, int line, const return 1; } +std::string escapeFilename(const std::string& filename) +{ + std::string escaped; + escaped.reserve(filename.size()); + + for (const char ch : filename) + { + switch (ch) + { + case '\\': + escaped.push_back('/'); + break; + case '"': + escaped.push_back('\\'); + escaped.push_back(ch); + break; + default: + escaped.push_back(ch); + } + } + + return escaped; +} + int main(int argc, char** argv) { Luau::assertHandler() = assertionHandler; @@ -330,6 +363,7 @@ int main(int argc, char** argv) CompileFormat compileFormat = CompileFormat::Text; Luau::CodeGen::AssemblyOptions::Target assemblyTarget = Luau::CodeGen::AssemblyOptions::Host; RecordStats recordStats = RecordStats::None; + std::string statsFile("stats.json"); for (int i = 1; i < argc; i++) { @@ -394,6 +428,16 @@ int main(int argc, char** argv) return 1; } } + else if (strncmp(argv[i], "--stats-file=", 13) == 0) + { + statsFile = argv[i] + 13; + + if (statsFile.size() == 0) + { + fprintf(stderr, "Error: filename missing for '--stats-file'.\n\n"); + return 1; + } + } else if (strncmp(argv[i], "--fflags=", 9) == 0) { setLuauFlags(argv[i] + 9); @@ -463,7 +507,7 @@ int main(int argc, char** argv) if (recordStats != RecordStats::None) { - FILE* fp = fopen("stats.json", "w"); + FILE* fp = fopen(statsFile.c_str(), "w"); if (!fp) { @@ -480,7 +524,8 @@ int main(int argc, char** argv) fprintf(fp, "{\n"); for (size_t i = 0; i < fileCount; ++i) { - fprintf(fp, "\"%s\": ", files[i].c_str()); + std::string escaped(escapeFilename(files[i])); + fprintf(fp, "\"%s\": ", escaped.c_str()); fileStats[i].serializeToJson(fp); fprintf(fp, i == (fileCount - 1) ? "\n" : ",\n"); } diff --git a/CodeGen/include/Luau/CodeGen.h b/CodeGen/include/Luau/CodeGen.h index 409bc22a..dfa3eeb0 100644 --- a/CodeGen/include/Luau/CodeGen.h +++ b/CodeGen/include/Luau/CodeGen.h @@ -80,6 +80,27 @@ struct AssemblyOptions void* annotatorContext = nullptr; }; +struct BlockLinearizationStats +{ + unsigned int constPropInstructionCount = 0; + double timeSeconds = 0.0; + + BlockLinearizationStats& operator+=(const BlockLinearizationStats& that) + { + this->constPropInstructionCount += that.constPropInstructionCount; + this->timeSeconds += that.timeSeconds; + + return *this; + } + + BlockLinearizationStats operator+(const BlockLinearizationStats& other) const + { + BlockLinearizationStats result(*this); + result += other; + return result; + } +}; + struct LoweringStats { unsigned totalFunctions = 0; @@ -94,6 +115,8 @@ struct LoweringStats int regAllocErrors = 0; int loweringErrors = 0; + BlockLinearizationStats blockLinearizationStats; + LoweringStats operator+(const LoweringStats& other) const { LoweringStats result(*this); @@ -113,6 +136,7 @@ struct LoweringStats this->maxBlockInstructions = std::max(this->maxBlockInstructions, that.maxBlockInstructions); this->regAllocErrors += that.regAllocErrors; this->loweringErrors += that.loweringErrors; + this->blockLinearizationStats += that.blockLinearizationStats; return *this; } }; diff --git a/CodeGen/include/Luau/IrData.h b/CodeGen/include/Luau/IrData.h index 19e082b5..9beee0ac 100644 --- a/CodeGen/include/Luau/IrData.h +++ b/CodeGen/include/Luau/IrData.h @@ -600,6 +600,10 @@ enum class IrCmd : uint8_t BITCOUNTLZ_UINT, BITCOUNTRZ_UINT, + // Swap byte order in A + // A: int + BYTESWAP_UINT, + // Calls native libm function with 1 or 2 arguments // A: builtin function ID // B: double diff --git a/CodeGen/src/CodeGenLower.h b/CodeGen/src/CodeGenLower.h index 8fcd832f..484d2dab 100644 --- a/CodeGen/src/CodeGenLower.h +++ b/CodeGen/src/CodeGenLower.h @@ -50,6 +50,13 @@ inline void gatherFunctions(std::vector& results, Proto* proto, unsigned gatherFunctions(results, proto->p[i], flags); } +inline unsigned getInstructionCount(const std::vector& instructions, IrCmd cmd) +{ + return unsigned(std::count_if(instructions.begin(), instructions.end(), [&cmd](const IrInst& inst) { + return inst.cmd == cmd; + })); +} + template inline bool lowerImpl(AssemblyBuilder& build, IrLowering& lowering, IrFunction& function, const std::vector& sortedBlocks, int bytecodeid, AssemblyOptions options) @@ -269,7 +276,25 @@ inline bool lowerFunction(IrBuilder& ir, AssemblyBuilder& build, ModuleHelpers& constPropInBlockChains(ir, useValueNumbering); if (!FFlag::DebugCodegenOptSize) + { + double startTime = 0.0; + unsigned constPropInstructionCount = 0; + + if (stats) + { + constPropInstructionCount = getInstructionCount(ir.function.instructions, IrCmd::SUBSTITUTE); + startTime = lua_clock(); + } + createLinearBlocks(ir, useValueNumbering); + + if (stats) + { + stats->blockLinearizationStats.timeSeconds += lua_clock() - startTime; + constPropInstructionCount = getInstructionCount(ir.function.instructions, IrCmd::SUBSTITUTE) - constPropInstructionCount; + stats->blockLinearizationStats.constPropInstructionCount += constPropInstructionCount; + } + } } std::vector sortedBlocks = getSortedBlockOrder(ir.function); diff --git a/CodeGen/src/CodeGenUtils.cpp b/CodeGen/src/CodeGenUtils.cpp index 3cdd20b3..9306ae4c 100644 --- a/CodeGen/src/CodeGenUtils.cpp +++ b/CodeGen/src/CodeGenUtils.cpp @@ -531,50 +531,6 @@ const Instruction* executeSETTABLEKS(lua_State* L, const Instruction* pc, StkId } } -const Instruction* executeNEWCLOSURE(lua_State* L, const Instruction* pc, StkId base, TValue* k) -{ - [[maybe_unused]] Closure* cl = clvalue(L->ci->func); - Instruction insn = *pc++; - StkId ra = VM_REG(LUAU_INSN_A(insn)); - - Proto* pv = cl->l.p->p[LUAU_INSN_D(insn)]; - LUAU_ASSERT(unsigned(LUAU_INSN_D(insn)) < unsigned(cl->l.p->sizep)); - - VM_PROTECT_PC(); // luaF_newLclosure may fail due to OOM - - // note: we save closure to stack early in case the code below wants to capture it by value - Closure* ncl = luaF_newLclosure(L, pv->nups, cl->env, pv); - setclvalue(L, ra, ncl); - - for (int ui = 0; ui < pv->nups; ++ui) - { - Instruction uinsn = *pc++; - LUAU_ASSERT(LUAU_INSN_OP(uinsn) == LOP_CAPTURE); - - switch (LUAU_INSN_A(uinsn)) - { - case LCT_VAL: - setobj(L, &ncl->l.uprefs[ui], VM_REG(LUAU_INSN_B(uinsn))); - break; - - case LCT_REF: - setupvalue(L, &ncl->l.uprefs[ui], luaF_findupval(L, VM_REG(LUAU_INSN_B(uinsn)))); - break; - - case LCT_UPVAL: - setobj(L, &ncl->l.uprefs[ui], VM_UV(LUAU_INSN_B(uinsn))); - break; - - default: - LUAU_ASSERT(!"Unknown upvalue capture type"); - LUAU_UNREACHABLE(); // improves switch() codegen by eliding opcode bounds checks - } - } - - VM_PROTECT(luaC_checkGC(L)); - return pc; -} - const Instruction* executeNAMECALL(lua_State* L, const Instruction* pc, StkId base, TValue* k) { [[maybe_unused]] Closure* cl = clvalue(L->ci->func); @@ -587,43 +543,19 @@ const Instruction* executeNAMECALL(lua_State* L, const Instruction* pc, StkId ba if (ttistable(rb)) { - Table* h = hvalue(rb); - // note: we can't use nodemask8 here because we need to query the main position of the table, and 8-bit nodemask8 only works - // for predictive lookups - LuaNode* n = &h->node[tsvalue(kv)->hash & (sizenode(h) - 1)]; + // note: lvmexecute.cpp version of NAMECALL has two fast paths, but both fast paths are inlined into IR + // as such, if we get here we can just use the generic path which makes the fallback path a little faster - const TValue* mt = 0; - const LuaNode* mtn = 0; - - // fast-path: key is in the table in expected slot - if (ttisstring(gkey(n)) && tsvalue(gkey(n)) == tsvalue(kv) && !ttisnil(gval(n))) - { - // note: order of copies allows rb to alias ra+1 or ra - setobj2s(L, ra + 1, rb); - setobj2s(L, ra, gval(n)); - } - // fast-path: key is absent from the base, table has an __index table, and it has the result in the expected slot - else if (gnext(n) == 0 && (mt = fasttm(L, hvalue(rb)->metatable, TM_INDEX)) && ttistable(mt) && - (mtn = &hvalue(mt)->node[LUAU_INSN_C(insn) & hvalue(mt)->nodemask8]) && ttisstring(gkey(mtn)) && tsvalue(gkey(mtn)) == tsvalue(kv) && - !ttisnil(gval(mtn))) - { - // note: order of copies allows rb to alias ra+1 or ra - setobj2s(L, ra + 1, rb); - setobj2s(L, ra, gval(mtn)); - } - else - { - // slow-path: handles full table lookup - setobj2s(L, ra + 1, rb); - L->cachedslot = LUAU_INSN_C(insn); - VM_PROTECT(luaV_gettable(L, rb, kv, ra)); - // save cachedslot to accelerate future lookups; patches currently executing instruction since pc-2 rolls back two pc++ - VM_PATCH_C(pc - 2, L->cachedslot); - // recompute ra since stack might have been reallocated - ra = VM_REG(LUAU_INSN_A(insn)); - if (ttisnil(ra)) - luaG_methoderror(L, ra + 1, tsvalue(kv)); - } + // slow-path: handles full table lookup + setobj2s(L, ra + 1, rb); + L->cachedslot = LUAU_INSN_C(insn); + VM_PROTECT(luaV_gettable(L, rb, kv, ra)); + // save cachedslot to accelerate future lookups; patches currently executing instruction since pc-2 rolls back two pc++ + VM_PATCH_C(pc - 2, L->cachedslot); + // recompute ra since stack might have been reallocated + ra = VM_REG(LUAU_INSN_A(insn)); + if (ttisnil(ra)) + luaG_methoderror(L, ra + 1, tsvalue(kv)); } else { diff --git a/CodeGen/src/CodeGenUtils.h b/CodeGen/src/CodeGenUtils.h index 7075e348..515a81f0 100644 --- a/CodeGen/src/CodeGenUtils.h +++ b/CodeGen/src/CodeGenUtils.h @@ -25,7 +25,6 @@ const Instruction* executeGETGLOBAL(lua_State* L, const Instruction* pc, StkId b const Instruction* executeSETGLOBAL(lua_State* L, const Instruction* pc, StkId base, TValue* k); const Instruction* executeGETTABLEKS(lua_State* L, const Instruction* pc, StkId base, TValue* k); const Instruction* executeSETTABLEKS(lua_State* L, const Instruction* pc, StkId base, TValue* k); -const Instruction* executeNEWCLOSURE(lua_State* L, const Instruction* pc, StkId base, TValue* k); const Instruction* executeNAMECALL(lua_State* L, const Instruction* pc, StkId base, TValue* k); const Instruction* executeSETLIST(lua_State* L, const Instruction* pc, StkId base, TValue* k); const Instruction* executeFORGPREP(lua_State* L, const Instruction* pc, StkId base, TValue* k); diff --git a/CodeGen/src/IrDump.cpp b/CodeGen/src/IrDump.cpp index 483e3e00..fd015b3a 100644 --- a/CodeGen/src/IrDump.cpp +++ b/CodeGen/src/IrDump.cpp @@ -309,6 +309,8 @@ const char* getCmdName(IrCmd cmd) return "BITCOUNTLZ_UINT"; case IrCmd::BITCOUNTRZ_UINT: return "BITCOUNTRZ_UINT"; + case IrCmd::BYTESWAP_UINT: + return "BYTESWAP_UINT"; case IrCmd::INVOKE_LIBM: return "INVOKE_LIBM"; case IrCmd::GET_TYPE: diff --git a/CodeGen/src/IrLoweringA64.cpp b/CodeGen/src/IrLoweringA64.cpp index 26a3b887..98beb513 100644 --- a/CodeGen/src/IrLoweringA64.cpp +++ b/CodeGen/src/IrLoweringA64.cpp @@ -1912,6 +1912,13 @@ void IrLoweringA64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next) build.clz(inst.regA64, inst.regA64); break; } + case IrCmd::BYTESWAP_UINT: + { + inst.regA64 = regs.allocReuse(KindA64::w, index, {inst.a}); + RegisterA64 temp = tempUint(inst.a); + build.rev(inst.regA64, temp); + break; + } case IrCmd::INVOKE_LIBM: { if (inst.c.kind != IrOpKind::None) diff --git a/CodeGen/src/IrLoweringX64.cpp b/CodeGen/src/IrLoweringX64.cpp index b9ff4f1f..df7488a9 100644 --- a/CodeGen/src/IrLoweringX64.cpp +++ b/CodeGen/src/IrLoweringX64.cpp @@ -822,7 +822,19 @@ void IrLoweringX64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next) case IrCmd::UINT_TO_NUM: inst.regX64 = regs.allocReg(SizeX64::xmmword, index); - build.vcvtsi2sd(inst.regX64, inst.regX64, qwordReg(regOp(inst.a))); + // AVX has no uint->double conversion; the source must come from UINT op and they all should clear top 32 bits so we can usually + // use 64-bit reg; the one exception is NUM_TO_UINT which doesn't clear top bits + if (IrCmd source = function.instOp(inst.a).cmd; source == IrCmd::NUM_TO_UINT) + { + ScopedRegX64 tmp{regs, SizeX64::dword}; + build.mov(tmp.reg, regOp(inst.a)); + build.vcvtsi2sd(inst.regX64, inst.regX64, qwordReg(tmp.reg)); + } + else + { + LUAU_ASSERT(source != IrCmd::SUBSTITUTE); // we don't process substitutions + build.vcvtsi2sd(inst.regX64, inst.regX64, qwordReg(regOp(inst.a))); + } break; case IrCmd::NUM_TO_INT: inst.regX64 = regs.allocReg(SizeX64::dword, index); @@ -1633,6 +1645,16 @@ void IrLoweringX64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next) build.setLabel(exit); break; } + case IrCmd::BYTESWAP_UINT: + { + inst.regX64 = regs.allocRegOrReuse(SizeX64::dword, index, {inst.a}); + + if (inst.a.kind != IrOpKind::Inst || inst.regX64 != regOp(inst.a)) + build.mov(inst.regX64, memRegUintOp(inst.a)); + + build.bswap(inst.regX64); + break; + } case IrCmd::INVOKE_LIBM: { IrCallWrapperX64 callWrap(regs, build, index); diff --git a/CodeGen/src/IrTranslateBuiltins.cpp b/CodeGen/src/IrTranslateBuiltins.cpp index 3b6b5def..b7d66ae6 100644 --- a/CodeGen/src/IrTranslateBuiltins.cpp +++ b/CodeGen/src/IrTranslateBuiltins.cpp @@ -583,8 +583,8 @@ static BuiltinImplResult translateBuiltinBit32ExtractK( return {BuiltinImplType::Full, 1}; } -static BuiltinImplResult translateBuiltinBit32Countz( - IrBuilder& build, LuauBuiltinFunction bfid, int nparams, int ra, int arg, IrOp args, int nresults, int pcpos) +static BuiltinImplResult translateBuiltinBit32Unary( + IrBuilder& build, IrCmd cmd, int nparams, int ra, int arg, IrOp args, int nresults, int pcpos) { if (nparams < 1 || nresults > 1) return {BuiltinImplType::None, -1}; @@ -594,7 +594,6 @@ static BuiltinImplResult translateBuiltinBit32Countz( IrOp vaui = build.inst(IrCmd::NUM_TO_UINT, va); - IrCmd cmd = (bfid == LBF_BIT32_COUNTLZ) ? IrCmd::BITCOUNTLZ_UINT : IrCmd::BITCOUNTRZ_UINT; IrOp bin = build.inst(cmd, vaui); IrOp value = build.inst(IrCmd::UINT_TO_NUM, bin); @@ -816,8 +815,9 @@ BuiltinImplResult translateBuiltin(IrBuilder& build, int bfid, int ra, int arg, case LBF_BIT32_EXTRACTK: return translateBuiltinBit32ExtractK(build, LuauBuiltinFunction(bfid), nparams, ra, arg, args, nresults, pcpos); case LBF_BIT32_COUNTLZ: + return translateBuiltinBit32Unary(build, IrCmd::BITCOUNTLZ_UINT, nparams, ra, arg, args, nresults, pcpos); case LBF_BIT32_COUNTRZ: - return translateBuiltinBit32Countz(build, LuauBuiltinFunction(bfid), nparams, ra, arg, args, nresults, pcpos); + return translateBuiltinBit32Unary(build, IrCmd::BITCOUNTRZ_UINT, nparams, ra, arg, args, nresults, pcpos); case LBF_BIT32_REPLACE: return translateBuiltinBit32Replace(build, LuauBuiltinFunction(bfid), nparams, ra, arg, args, nresults, fallback, pcpos); case LBF_TYPE: @@ -830,6 +830,8 @@ BuiltinImplResult translateBuiltin(IrBuilder& build, int bfid, int ra, int arg, return translateBuiltinTableInsert(build, nparams, ra, arg, args, nresults, pcpos); case LBF_STRING_LEN: return translateBuiltinStringLen(build, nparams, ra, arg, args, nresults, pcpos); + case LBF_BIT32_BYTESWAP: + return translateBuiltinBit32Unary(build, IrCmd::BYTESWAP_UINT, nparams, ra, arg, args, nresults, pcpos); default: return {BuiltinImplType::None, -1}; } diff --git a/CodeGen/src/IrUtils.cpp b/CodeGen/src/IrUtils.cpp index ba46dbc9..5e606481 100644 --- a/CodeGen/src/IrUtils.cpp +++ b/CodeGen/src/IrUtils.cpp @@ -163,6 +163,7 @@ IrValueKind getCmdValueKind(IrCmd cmd) case IrCmd::BITRROTATE_UINT: case IrCmd::BITCOUNTLZ_UINT: case IrCmd::BITCOUNTRZ_UINT: + case IrCmd::BYTESWAP_UINT: return IrValueKind::Int; case IrCmd::INVOKE_LIBM: return IrValueKind::Double; diff --git a/CodeGen/src/NativeState.cpp b/CodeGen/src/NativeState.cpp index 7b2f068b..a161987d 100644 --- a/CodeGen/src/NativeState.cpp +++ b/CodeGen/src/NativeState.cpp @@ -103,7 +103,6 @@ void initFunctions(NativeState& data) data.context.executeGETTABLEKS = executeGETTABLEKS; data.context.executeSETTABLEKS = executeSETTABLEKS; - data.context.executeNEWCLOSURE = executeNEWCLOSURE; data.context.executeNAMECALL = executeNAMECALL; data.context.executeFORGPREP = executeFORGPREP; data.context.executeGETVARARGSMultRet = executeGETVARARGSMultRet; diff --git a/CodeGen/src/NativeState.h b/CodeGen/src/NativeState.h index f0b8561c..7670482d 100644 --- a/CodeGen/src/NativeState.h +++ b/CodeGen/src/NativeState.h @@ -94,7 +94,6 @@ struct NativeContext const Instruction* (*executeSETGLOBAL)(lua_State* L, const Instruction* pc, StkId base, TValue* k) = nullptr; const Instruction* (*executeGETTABLEKS)(lua_State* L, const Instruction* pc, StkId base, TValue* k) = nullptr; const Instruction* (*executeSETTABLEKS)(lua_State* L, const Instruction* pc, StkId base, TValue* k) = nullptr; - const Instruction* (*executeNEWCLOSURE)(lua_State* L, const Instruction* pc, StkId base, TValue* k) = nullptr; const Instruction* (*executeNAMECALL)(lua_State* L, const Instruction* pc, StkId base, TValue* k) = nullptr; const Instruction* (*executeSETLIST)(lua_State* L, const Instruction* pc, StkId base, TValue* k) = nullptr; const Instruction* (*executeFORGPREP)(lua_State* L, const Instruction* pc, StkId base, TValue* k) = nullptr; diff --git a/CodeGen/src/OptimizeConstProp.cpp b/CodeGen/src/OptimizeConstProp.cpp index 3315ec96..a37b810d 100644 --- a/CodeGen/src/OptimizeConstProp.cpp +++ b/CodeGen/src/OptimizeConstProp.cpp @@ -1168,6 +1168,7 @@ static void constPropInInst(ConstPropState& state, IrBuilder& build, IrFunction& case IrCmd::BITLROTATE_UINT: case IrCmd::BITCOUNTLZ_UINT: case IrCmd::BITCOUNTRZ_UINT: + case IrCmd::BYTESWAP_UINT: case IrCmd::INVOKE_LIBM: case IrCmd::GET_TYPE: case IrCmd::GET_TYPEOF: diff --git a/Compiler/include/Luau/BytecodeBuilder.h b/Compiler/include/Luau/BytecodeBuilder.h index f5098d17..2d86e412 100644 --- a/Compiler/include/Luau/BytecodeBuilder.h +++ b/Compiler/include/Luau/BytecodeBuilder.h @@ -83,6 +83,7 @@ public: void pushDebugUpval(StringRef name); size_t getInstructionCount() const; + size_t getTotalInstructionCount() const; uint32_t getDebugPC() const; void addDebugRemark(const char* format, ...) LUAU_PRINTF_ATTR(2, 3); @@ -232,6 +233,7 @@ private: uint32_t currentFunction = ~0u; uint32_t mainFunction = ~0u; + size_t totalInstructionCount = 0; std::vector insns; std::vector lines; std::vector constants; diff --git a/Compiler/src/BytecodeBuilder.cpp b/Compiler/src/BytecodeBuilder.cpp index 83fb9ce5..7f545282 100644 --- a/Compiler/src/BytecodeBuilder.cpp +++ b/Compiler/src/BytecodeBuilder.cpp @@ -244,6 +244,7 @@ void BytecodeBuilder::endFunction(uint8_t maxstacksize, uint8_t numupvalues, uin currentFunction = ~0u; + totalInstructionCount += insns.size(); insns.clear(); lines.clear(); constants.clear(); @@ -539,6 +540,11 @@ size_t BytecodeBuilder::getInstructionCount() const return insns.size(); } +size_t BytecodeBuilder::getTotalInstructionCount() const +{ + return totalInstructionCount; +} + uint32_t BytecodeBuilder::getDebugPC() const { return uint32_t(insns.size()); diff --git a/Compiler/src/Compiler.cpp b/Compiler/src/Compiler.cpp index e0a0cac8..c685ffbd 100644 --- a/Compiler/src/Compiler.cpp +++ b/Compiler/src/Compiler.cpp @@ -27,7 +27,6 @@ LUAU_FASTINTVARIABLE(LuauCompileInlineThresholdMaxBoost, 300) LUAU_FASTINTVARIABLE(LuauCompileInlineDepth, 5) LUAU_FASTFLAG(LuauFloorDivision) -LUAU_FASTFLAGVARIABLE(LuauCompileFixContinueValidation2, false) LUAU_FASTFLAGVARIABLE(LuauCompileIfElseAndOr, false) namespace Luau @@ -2519,14 +2518,9 @@ struct Compiler // Optimization: body is a "continue" statement with no "else" => we can directly continue in "then" case if (!stat->elsebody && continueStatement != nullptr && !areLocalsCaptured(loops.back().localOffsetContinue)) { - if (FFlag::LuauCompileFixContinueValidation2) - { - // track continue statement for repeat..until validation (validateContinueUntil) - if (!loops.back().continueUsed) - loops.back().continueUsed = continueStatement; - } - else if (loops.back().untilCondition) - validateContinueUntil(continueStatement, loops.back().untilCondition); + // track continue statement for repeat..until validation (validateContinueUntil) + if (!loops.back().continueUsed) + loops.back().continueUsed = continueStatement; // fallthrough = proceed with the loop body as usual std::vector elseJump; @@ -2587,7 +2581,7 @@ struct Compiler size_t oldJumps = loopJumps.size(); size_t oldLocals = localStack.size(); - loops.push_back({oldLocals, oldLocals, nullptr, nullptr}); + loops.push_back({oldLocals, oldLocals, nullptr}); hasLoops = true; size_t loopLabel = bytecode.emitLabel(); @@ -2623,7 +2617,7 @@ struct Compiler size_t oldJumps = loopJumps.size(); size_t oldLocals = localStack.size(); - loops.push_back({oldLocals, oldLocals, stat->condition, nullptr}); + loops.push_back({oldLocals, oldLocals, nullptr}); hasLoops = true; size_t loopLabel = bytecode.emitLabel(); @@ -2648,7 +2642,7 @@ struct Compiler // if continue was called from this statement, then any local defined after this in the loop body should not be accessed by until condition // it is sufficient to check this condition once, as if this holds for the first continue, it must hold for all subsequent continues. - if (FFlag::LuauCompileFixContinueValidation2 && loops.back().continueUsed && !continueValidated) + if (loops.back().continueUsed && !continueValidated) { validateContinueUntil(loops.back().continueUsed, stat->condition, body, i + 1); continueValidated = true; @@ -2870,7 +2864,7 @@ struct Compiler size_t oldLocals = localStack.size(); size_t oldJumps = loopJumps.size(); - loops.push_back({oldLocals, oldLocals, nullptr, nullptr}); + loops.push_back({oldLocals, oldLocals, nullptr}); for (int iv = 0; iv < tripCount; ++iv) { @@ -2921,7 +2915,7 @@ struct Compiler size_t oldLocals = localStack.size(); size_t oldJumps = loopJumps.size(); - loops.push_back({oldLocals, oldLocals, nullptr, nullptr}); + loops.push_back({oldLocals, oldLocals, nullptr}); hasLoops = true; // register layout: limit, step, index @@ -2986,7 +2980,7 @@ struct Compiler size_t oldLocals = localStack.size(); size_t oldJumps = loopJumps.size(); - loops.push_back({oldLocals, oldLocals, nullptr, nullptr}); + loops.push_back({oldLocals, oldLocals, nullptr}); hasLoops = true; // register layout: generator, state, index, variables... @@ -3398,14 +3392,9 @@ struct Compiler { LUAU_ASSERT(!loops.empty()); - if (FFlag::LuauCompileFixContinueValidation2) - { - // track continue statement for repeat..until validation (validateContinueUntil) - if (!loops.back().continueUsed) - loops.back().continueUsed = stat; - } - else if (loops.back().untilCondition) - validateContinueUntil(stat, loops.back().untilCondition); + // track continue statement for repeat..until validation (validateContinueUntil) + if (!loops.back().continueUsed) + loops.back().continueUsed = stat; // before continuing, we need to close all local variables that were captured in closures since loop start // normally they are closed by the enclosing blocks, including the loop block, but we're skipping that here @@ -3488,21 +3477,8 @@ struct Compiler } } - void validateContinueUntil(AstStat* cont, AstExpr* condition) - { - LUAU_ASSERT(!FFlag::LuauCompileFixContinueValidation2); - UndefinedLocalVisitor visitor(this); - condition->visit(&visitor); - - if (visitor.undef) - CompileError::raise(condition->location, - "Local %s used in the repeat..until condition is undefined because continue statement on line %d jumps over it", - visitor.undef->name.value, cont->location.begin.line + 1); - } - void validateContinueUntil(AstStat* cont, AstExpr* condition, AstStatBlock* body, size_t start) { - LUAU_ASSERT(FFlag::LuauCompileFixContinueValidation2); UndefinedLocalVisitor visitor(this); for (size_t i = start; i < body->body.size; ++i) @@ -3748,18 +3724,8 @@ struct Compiler void check(AstLocal* local) { - if (FFlag::LuauCompileFixContinueValidation2) - { - if (!undef && locals.contains(local)) - undef = local; - } - else - { - Local& l = self->locals[local]; - - if (!l.allocated && !undef) - undef = local; - } + if (!undef && locals.contains(local)) + undef = local; } bool visit(AstExprLocal* node) override @@ -3904,9 +3870,6 @@ struct Compiler size_t localOffset; size_t localOffsetContinue; - // TODO: Remove with LuauCompileFixContinueValidation2 - AstExpr* untilCondition; - AstStatContinue* continueUsed; }; diff --git a/Sources.cmake b/Sources.cmake index 2604514e..fb883d7d 100644 --- a/Sources.cmake +++ b/Sources.cmake @@ -156,7 +156,7 @@ target_sources(Luau.Analysis PRIVATE Analysis/include/Luau/Cancellation.h Analysis/include/Luau/Clone.h Analysis/include/Luau/Constraint.h - Analysis/include/Luau/ConstraintGraphBuilder.h + Analysis/include/Luau/ConstraintGenerator.h Analysis/include/Luau/ConstraintSolver.h Analysis/include/Luau/ControlFlow.h Analysis/include/Luau/DataFlowGraph.h @@ -223,7 +223,7 @@ target_sources(Luau.Analysis PRIVATE Analysis/src/BuiltinDefinitions.cpp Analysis/src/Clone.cpp Analysis/src/Constraint.cpp - Analysis/src/ConstraintGraphBuilder.cpp + Analysis/src/ConstraintGenerator.cpp Analysis/src/ConstraintSolver.cpp Analysis/src/DataFlowGraph.cpp Analysis/src/DcrLogger.cpp @@ -385,8 +385,8 @@ if(TARGET Luau.UnitTest) tests/CodeAllocator.test.cpp tests/Compiler.test.cpp tests/Config.test.cpp - tests/ConstraintGraphBuilderFixture.cpp - tests/ConstraintGraphBuilderFixture.h + tests/ConstraintGeneratorFixture.cpp + tests/ConstraintGeneratorFixture.h tests/ConstraintSolver.test.cpp tests/CostModel.test.cpp tests/DataFlowGraph.test.cpp diff --git a/VM/src/lbuiltins.cpp b/VM/src/lbuiltins.cpp index e5e2bac6..e28bb169 100644 --- a/VM/src/lbuiltins.cpp +++ b/VM/src/lbuiltins.cpp @@ -1353,7 +1353,7 @@ static int luauF_readinteger(lua_State* L, StkId res, TValue* arg0, int nresults return -1; T val; - memcpy(&val, (char*)bufvalue(arg0)->data + offset, sizeof(T)); + memcpy(&val, (char*)bufvalue(arg0)->data + unsigned(offset), sizeof(T)); setnvalue(res, double(val)); return 1; } @@ -1378,7 +1378,7 @@ static int luauF_writeinteger(lua_State* L, StkId res, TValue* arg0, int nresult luai_num2unsigned(value, incoming); T val = T(value); - memcpy((char*)bufvalue(arg0)->data + offset, &val, sizeof(T)); + memcpy((char*)bufvalue(arg0)->data + unsigned(offset), &val, sizeof(T)); return 0; } #endif @@ -1398,7 +1398,12 @@ static int luauF_readfp(lua_State* L, StkId res, TValue* arg0, int nresults, Stk return -1; T val; - memcpy(&val, (char*)bufvalue(arg0)->data + offset, sizeof(T)); +#ifdef _MSC_VER + // avoid memcpy path on MSVC because it results in integer stack copy + floating-point ops on stack + val = *(T*)((char*)bufvalue(arg0)->data + unsigned(offset)); +#else + memcpy(&val, (char*)bufvalue(arg0)->data + unsigned(offset), sizeof(T)); +#endif setnvalue(res, double(val)); return 1; } @@ -1419,7 +1424,12 @@ static int luauF_writefp(lua_State* L, StkId res, TValue* arg0, int nresults, St return -1; T val = T(nvalue(args + 1)); - memcpy((char*)bufvalue(arg0)->data + offset, &val, sizeof(T)); +#ifdef _MSC_VER + // avoid memcpy path on MSVC because it results in integer stack copy + floating-point ops on stack + *(T*)((char*)bufvalue(arg0)->data + unsigned(offset)) = val; +#else + memcpy((char*)bufvalue(arg0)->data + unsigned(offset), &val, sizeof(T)); +#endif return 0; } #endif diff --git a/VM/src/ldo.cpp b/VM/src/ldo.cpp index 6729f155..d13e98f3 100644 --- a/VM/src/ldo.cpp +++ b/VM/src/ldo.cpp @@ -17,8 +17,6 @@ #include -LUAU_DYNAMIC_FASTFLAGVARIABLE(LuauHandlerClose, false) - /* ** {====================================================== ** Error-recovery functions @@ -409,7 +407,7 @@ static void resume_handle(lua_State* L, void* ud) L->ci = restoreci(L, old_ci); // close eventual pending closures; this means it's now safe to restore stack - luaF_close(L, DFFlag::LuauHandlerClose ? L->ci->base : L->base); + luaF_close(L, L->ci->base); // finish cont call and restore stack to previous ci top luau_poscall(L, L->top - n); diff --git a/bench/tests/sha256.lua b/bench/tests/sha256.lua index 0e4227a3..a01e801e 100644 --- a/bench/tests/sha256.lua +++ b/bench/tests/sha256.lua @@ -132,7 +132,8 @@ function test() local ts0 = os.clock() for i = 1, 100 do - sha256(input) + local res = sha256(input) + assert(res == "45849646c50337988ccc877d23fcc0de50d1df7490fdc3b9333aed0de8ab492a") end local ts1 = os.clock() diff --git a/tests/Compiler.test.cpp b/tests/Compiler.test.cpp index bc9a12ea..35dfd230 100644 --- a/tests/Compiler.test.cpp +++ b/tests/Compiler.test.cpp @@ -1909,8 +1909,6 @@ RETURN R0 0 TEST_CASE("LoopContinueIgnoresImplicitConstant") { - ScopedFastFlag luauCompileFixContinueValidation{"LuauCompileFixContinueValidation2", true}; - // this used to crash the compiler :( CHECK_EQ("\n" + compileFunction0(R"( local _ @@ -1926,8 +1924,6 @@ RETURN R0 0 TEST_CASE("LoopContinueIgnoresExplicitConstant") { - ScopedFastFlag luauCompileFixContinueValidation{"LuauCompileFixContinueValidation2", true}; - // Constants do not allocate locals and 'continue' validation should skip them if their lifetime already started CHECK_EQ("\n" + compileFunction0(R"( local c = true @@ -1943,8 +1939,6 @@ RETURN R0 0 TEST_CASE("LoopContinueRespectsExplicitConstant") { - ScopedFastFlag luauCompileFixContinueValidation{"LuauCompileFixContinueValidation2", true}; - // If local lifetime hasn't started, even if it's a constant that will not receive an allocation, it cannot be jumped over try { @@ -1969,8 +1963,6 @@ until c TEST_CASE("LoopContinueIgnoresImplicitConstantAfterInline") { - ScopedFastFlag luauCompileFixContinueValidation{"LuauCompileFixContinueValidation2", true}; - // Inlining might also replace some locals with constants instead of allocating them CHECK_EQ("\n" + compileFunction(R"( local function inline(f) @@ -1994,7 +1986,6 @@ RETURN R0 0 TEST_CASE("LoopContinueCorrectlyHandlesImplicitConstantAfterUnroll") { - ScopedFastFlag sff{"LuauCompileFixContinueValidation2", true}; ScopedFastInt sfi("LuauCompileLoopUnrollThreshold", 200); // access to implicit constant that depends on the unrolled loop constant is still invalid even though we can constant-propagate it diff --git a/tests/Conformance.test.cpp b/tests/Conformance.test.cpp index b7f77711..2a2017a6 100644 --- a/tests/Conformance.test.cpp +++ b/tests/Conformance.test.cpp @@ -435,8 +435,6 @@ static int cxxthrow(lua_State* L) TEST_CASE("PCall") { - ScopedFastFlag sff("LuauHandlerClose", true); - runConformance( "pcall.lua", [](lua_State* L) { diff --git a/tests/ConstraintGraphBuilderFixture.cpp b/tests/ConstraintGeneratorFixture.cpp similarity index 62% rename from tests/ConstraintGraphBuilderFixture.cpp rename to tests/ConstraintGeneratorFixture.cpp index 293c26ff..dc1aea80 100644 --- a/tests/ConstraintGraphBuilderFixture.cpp +++ b/tests/ConstraintGeneratorFixture.cpp @@ -1,10 +1,10 @@ // This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details -#include "ConstraintGraphBuilderFixture.h" +#include "ConstraintGeneratorFixture.h" namespace Luau { -ConstraintGraphBuilderFixture::ConstraintGraphBuilderFixture() +ConstraintGeneratorFixture::ConstraintGeneratorFixture() : Fixture() , mainModule(new Module) , forceTheFlag{"DebugLuauDeferredConstraintResolution", true} @@ -15,18 +15,18 @@ ConstraintGraphBuilderFixture::ConstraintGraphBuilderFixture() BlockedTypePack::nextIndex = 0; } -void ConstraintGraphBuilderFixture::generateConstraints(const std::string& code) +void ConstraintGeneratorFixture::generateConstraints(const std::string& code) { AstStatBlock* root = parse(code); dfg = std::make_unique(DataFlowGraphBuilder::build(root, NotNull{&ice})); - cgb = std::make_unique(mainModule, NotNull{&normalizer}, NotNull(&moduleResolver), builtinTypes, NotNull(&ice), + cg = std::make_unique(mainModule, NotNull{&normalizer}, NotNull(&moduleResolver), builtinTypes, NotNull(&ice), frontend.globals.globalScope, /*prepareModuleScope*/ nullptr, &logger, NotNull{dfg.get()}, std::vector()); - cgb->visitModuleRoot(root); - rootScope = cgb->rootScope; - constraints = Luau::borrowConstraints(cgb->constraints); + cg->visitModuleRoot(root); + rootScope = cg->rootScope; + constraints = Luau::borrowConstraints(cg->constraints); } -void ConstraintGraphBuilderFixture::solve(const std::string& code) +void ConstraintGeneratorFixture::solve(const std::string& code) { generateConstraints(code); ConstraintSolver cs{NotNull{&normalizer}, NotNull{rootScope}, constraints, "MainModule", NotNull(&moduleResolver), {}, &logger, {}}; diff --git a/tests/ConstraintGraphBuilderFixture.h b/tests/ConstraintGeneratorFixture.h similarity index 81% rename from tests/ConstraintGraphBuilderFixture.h rename to tests/ConstraintGeneratorFixture.h index 5e7fedab..ff362be1 100644 --- a/tests/ConstraintGraphBuilderFixture.h +++ b/tests/ConstraintGeneratorFixture.h @@ -1,7 +1,7 @@ // This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details #pragma once -#include "Luau/ConstraintGraphBuilder.h" +#include "Luau/ConstraintGenerator.h" #include "Luau/ConstraintSolver.h" #include "Luau/DcrLogger.h" #include "Luau/TypeArena.h" @@ -13,7 +13,7 @@ namespace Luau { -struct ConstraintGraphBuilderFixture : Fixture +struct ConstraintGeneratorFixture : Fixture { TypeArena arena; ModulePtr mainModule; @@ -22,14 +22,14 @@ struct ConstraintGraphBuilderFixture : Fixture Normalizer normalizer{&arena, builtinTypes, NotNull{&sharedState}}; std::unique_ptr dfg; - std::unique_ptr cgb; + std::unique_ptr cg; Scope* rootScope = nullptr; std::vector> constraints; ScopedFastFlag forceTheFlag; - ConstraintGraphBuilderFixture(); + ConstraintGeneratorFixture(); void generateConstraints(const std::string& code); void solve(const std::string& code); diff --git a/tests/ConstraintSolver.test.cpp b/tests/ConstraintSolver.test.cpp index 32cb3cda..204d4d14 100644 --- a/tests/ConstraintSolver.test.cpp +++ b/tests/ConstraintSolver.test.cpp @@ -1,6 +1,6 @@ // This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details -#include "ConstraintGraphBuilderFixture.h" +#include "ConstraintGeneratorFixture.h" #include "Fixture.h" #include "doctest.h" @@ -17,7 +17,7 @@ static TypeId requireBinding(Scope* scope, const char* name) TEST_SUITE_BEGIN("ConstraintSolver"); -TEST_CASE_FIXTURE(ConstraintGraphBuilderFixture, "hello") +TEST_CASE_FIXTURE(ConstraintGeneratorFixture, "hello") { solve(R"( local a = 55 @@ -29,7 +29,7 @@ TEST_CASE_FIXTURE(ConstraintGraphBuilderFixture, "hello") CHECK("number" == toString(bType)); } -TEST_CASE_FIXTURE(ConstraintGraphBuilderFixture, "generic_function") +TEST_CASE_FIXTURE(ConstraintGeneratorFixture, "generic_function") { solve(R"( local function id(a) @@ -42,7 +42,7 @@ TEST_CASE_FIXTURE(ConstraintGraphBuilderFixture, "generic_function") CHECK("(a) -> a" == toString(idType)); } -TEST_CASE_FIXTURE(ConstraintGraphBuilderFixture, "proper_let_generalization") +TEST_CASE_FIXTURE(ConstraintGeneratorFixture, "proper_let_generalization") { solve(R"( local function a(c) diff --git a/tests/Error.test.cpp b/tests/Error.test.cpp index 0a71794f..a1869e88 100644 --- a/tests/Error.test.cpp +++ b/tests/Error.test.cpp @@ -17,7 +17,7 @@ TEST_CASE("TypeError_code_should_return_nonzero_code") TEST_CASE_FIXTURE(BuiltinsFixture, "metatable_names_show_instead_of_tables") { frontend.options.retainFullTypeGraphs = false; - ScopedFastFlag sff{"LuauStacklessTypeClone2", true}; + ScopedFastFlag sff{"LuauStacklessTypeClone3", true}; CheckResult result = check(R"( --!strict local Account = {} diff --git a/tests/IrBuilder.test.cpp b/tests/IrBuilder.test.cpp index 4388c400..bd69cb13 100644 --- a/tests/IrBuilder.test.cpp +++ b/tests/IrBuilder.test.cpp @@ -3106,3 +3106,37 @@ bb_1: } TEST_SUITE_END(); + +TEST_SUITE_BEGIN("Dump"); + +TEST_CASE_FIXTURE(IrBuilderFixture, "ToDot") +{ + IrOp entry = build.block(IrBlockKind::Internal); + IrOp a = build.block(IrBlockKind::Internal); + IrOp b = build.block(IrBlockKind::Internal); + IrOp exit = build.block(IrBlockKind::Internal); + + build.beginBlock(entry); + build.inst(IrCmd::JUMP_EQ_TAG, build.inst(IrCmd::LOAD_TAG, build.vmReg(0)), build.constTag(tnumber), a, b); + + build.beginBlock(a); + build.inst(IrCmd::STORE_TVALUE, build.vmReg(2), build.inst(IrCmd::LOAD_TVALUE, build.vmReg(1))); + build.inst(IrCmd::JUMP, exit); + + build.beginBlock(b); + build.inst(IrCmd::STORE_TVALUE, build.vmReg(3), build.inst(IrCmd::LOAD_TVALUE, build.vmReg(1))); + build.inst(IrCmd::JUMP, exit); + + build.beginBlock(exit); + build.inst(IrCmd::RETURN, build.vmReg(2), build.constInt(2)); + + updateUseCounts(build.function); + computeCfgInfo(build.function); + + // note: we don't validate the output of these to avoid test churn when dot formatting changes, but we run these to make sure they don't assert/crash + toDot(build.function, /* includeInst= */ true); + toDotCfg(build.function); + toDotDjGraph(build.function); +} + +TEST_SUITE_END(); diff --git a/tests/Linter.test.cpp b/tests/Linter.test.cpp index 9907d7a1..f71d92ad 100644 --- a/tests/Linter.test.cpp +++ b/tests/Linter.test.cpp @@ -1517,8 +1517,6 @@ end TEST_CASE_FIXTURE(BuiltinsFixture, "DeprecatedApiFenv") { - ScopedFastFlag sff("LuauLintDeprecatedFenv", true); - LintResult result = lint(R"( local f, g, h = ... @@ -1591,8 +1589,6 @@ table.create(42, {} :: {}) TEST_CASE_FIXTURE(BuiltinsFixture, "TableOperationsIndexer") { - ScopedFastFlag sff("LuauLintTableIndexer", true); - LintResult result = lint(R"( local t1 = {} -- ok: empty local t2 = {1, 2} -- ok: array @@ -1827,8 +1823,71 @@ local _ = 0x10000000000000000 )"); REQUIRE(2 == result.warnings.size()); - CHECK_EQ(result.warnings[0].text, "Binary number literal exceeded available precision and has been truncated to 2^64"); - CHECK_EQ(result.warnings[1].text, "Hexadecimal number literal exceeded available precision and has been truncated to 2^64"); + CHECK_EQ(result.warnings[0].text, "Binary number literal exceeded available precision and was truncated to 2^64"); + CHECK_EQ(result.warnings[1].text, "Hexadecimal number literal exceeded available precision and was truncated to 2^64"); +} + +TEST_CASE_FIXTURE(Fixture, "IntegerParsingDecimalImprecise") +{ + ScopedFastFlag sff("LuauParseImpreciseNumber", true); + + LintResult result = lint(R"( +local _ = 10000000000000000000000000000000000000000000000000000000000000000 +local _ = 10000000000000001 +local _ = -10000000000000001 + +-- 10^16 = 2^16 * 5^16, 5^16 only requires 38 bits +local _ = 10000000000000000 +local _ = -10000000000000000 + +-- smallest possible number that is parsed imprecisely +local _ = 9007199254740993 +local _ = -9007199254740993 + +-- note that numbers before and after parse precisely (number after is even => 1 more mantissa bit) +local _ = 9007199254740992 +local _ = 9007199254740994 + +-- large powers of two should work as well (this is 2^63) +local _ = -9223372036854775808 +)"); + + REQUIRE(5 == result.warnings.size()); + CHECK_EQ(result.warnings[0].text, "Number literal exceeded available precision and was truncated to closest representable number"); + CHECK_EQ(result.warnings[0].location.begin.line, 1); + CHECK_EQ(result.warnings[1].text, "Number literal exceeded available precision and was truncated to closest representable number"); + CHECK_EQ(result.warnings[1].location.begin.line, 2); + CHECK_EQ(result.warnings[2].text, "Number literal exceeded available precision and was truncated to closest representable number"); + CHECK_EQ(result.warnings[2].location.begin.line, 3); + CHECK_EQ(result.warnings[3].text, "Number literal exceeded available precision and was truncated to closest representable number"); + CHECK_EQ(result.warnings[3].location.begin.line, 10); + CHECK_EQ(result.warnings[4].text, "Number literal exceeded available precision and was truncated to closest representable number"); + CHECK_EQ(result.warnings[4].location.begin.line, 11); +} + +TEST_CASE_FIXTURE(Fixture, "IntegerParsingHexImprecise") +{ + ScopedFastFlag sff("LuauParseImpreciseNumber", true); + + LintResult result = lint(R"( +local _ = 0x1234567812345678 + +-- smallest possible number that is parsed imprecisely +local _ = 0x20000000000001 + +-- note that numbers before and after parse precisely (number after is even => 1 more mantissa bit) +local _ = 0x20000000000000 +local _ = 0x20000000000002 + +-- large powers of two should work as well (this is 2^63) +local _ = -9223372036854775808 +)"); + + REQUIRE(2 == result.warnings.size()); + CHECK_EQ(result.warnings[0].text, "Number literal exceeded available precision and was truncated to closest representable number"); + CHECK_EQ(result.warnings[0].location.begin.line, 1); + CHECK_EQ(result.warnings[1].text, "Number literal exceeded available precision and was truncated to closest representable number"); + CHECK_EQ(result.warnings[1].location.begin.line, 4); } TEST_CASE_FIXTURE(Fixture, "ComparisonPrecedence") diff --git a/tests/Module.test.cpp b/tests/Module.test.cpp index 208db5d8..c966968f 100644 --- a/tests/Module.test.cpp +++ b/tests/Module.test.cpp @@ -14,7 +14,7 @@ using namespace Luau; LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution); -LUAU_FASTFLAG(LuauStacklessTypeClone2) +LUAU_FASTFLAG(LuauStacklessTypeClone3) TEST_SUITE_BEGIN("ModuleTests"); @@ -336,7 +336,7 @@ TEST_CASE_FIXTURE(Fixture, "clone_recursion_limit") int limit = 400; #endif - ScopedFastFlag sff{"LuauStacklessTypeClone2", false}; + ScopedFastFlag sff{"LuauStacklessTypeClone3", false}; ScopedFastInt luauTypeCloneRecursionLimit{"LuauTypeCloneRecursionLimit", limit}; TypeArena src; @@ -360,7 +360,7 @@ TEST_CASE_FIXTURE(Fixture, "clone_recursion_limit") TEST_CASE_FIXTURE(Fixture, "clone_iteration_limit") { - ScopedFastFlag sff{"LuauStacklessTypeClone2", true}; + ScopedFastFlag sff{"LuauStacklessTypeClone3", true}; ScopedFastInt sfi{"LuauTypeCloneIterationLimit", 500}; TypeArena src; @@ -534,4 +534,36 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "clone_table_bound_to_table_bound_to_table") REQUIRE(!tableA->boundTo); } +TEST_CASE_FIXTURE(BuiltinsFixture, "clone_a_bound_type_to_a_persistent_type") +{ + ScopedFastFlag sff{"LuauStacklessTypeClone3", true}; + + TypeArena arena; + + TypeId boundTo = arena.addType(BoundType{builtinTypes->numberType}); + REQUIRE(builtinTypes->numberType->persistent); + + TypeArena dest; + CloneState state{builtinTypes}; + TypeId res = clone(boundTo, dest, state); + + REQUIRE(res == follow(boundTo)); +} + +TEST_CASE_FIXTURE(BuiltinsFixture, "clone_a_bound_typepack_to_a_persistent_typepack") +{ + ScopedFastFlag sff{"LuauStacklessTypeClone3", true}; + + TypeArena arena; + + TypePackId boundTo = arena.addTypePack(BoundTypePack{builtinTypes->neverTypePack}); + REQUIRE(builtinTypes->neverTypePack->persistent); + + TypeArena dest; + CloneState state{builtinTypes}; + TypePackId res = clone(boundTo, dest, state); + + REQUIRE(res == follow(boundTo)); +} + TEST_SUITE_END(); diff --git a/tests/Normalize.test.cpp b/tests/Normalize.test.cpp index ee818022..30ec3316 100644 --- a/tests/Normalize.test.cpp +++ b/tests/Normalize.test.cpp @@ -31,6 +31,9 @@ struct IsSubtypeFixture : Fixture bool isConsistentSubtype(TypeId a, TypeId b) { + // any test that is testing isConsistentSubtype is testing the old solver exclusively! + ScopedFastFlag noDcr{"DebugLuauDeferredConstraintResolution", false}; + Location location; ModulePtr module = getMainModule(); REQUIRE(module); @@ -169,7 +172,10 @@ TEST_CASE_FIXTURE(IsSubtypeFixture, "table_with_union_prop") TypeId a = requireType("a"); TypeId b = requireType("b"); - CHECK(isSubtype(a, b)); + if (FFlag::DebugLuauDeferredConstraintResolution) + CHECK(!isSubtype(a, b)); // table properties are invariant + else + CHECK(isSubtype(a, b)); CHECK(!isSubtype(b, a)); } @@ -187,7 +193,10 @@ TEST_CASE_FIXTURE(IsSubtypeFixture, "table_with_any_prop") TypeId a = requireType("a"); TypeId b = requireType("b"); - CHECK(isSubtype(a, b)); + if (FFlag::DebugLuauDeferredConstraintResolution) + CHECK(!isSubtype(a, b)); // table properties are invariant + else + CHECK(isSubtype(a, b)); CHECK(!isSubtype(b, a)); CHECK(isConsistentSubtype(b, a)); } @@ -249,7 +258,10 @@ TEST_CASE_FIXTURE(IsSubtypeFixture, "tables") TypeId c = requireType("c"); TypeId d = requireType("d"); - CHECK(isSubtype(a, b)); + if (FFlag::DebugLuauDeferredConstraintResolution) + CHECK(!isSubtype(a, b)); // table properties are invariant + else + CHECK(isSubtype(a, b)); CHECK(!isSubtype(b, a)); CHECK(isConsistentSubtype(b, a)); @@ -259,7 +271,10 @@ TEST_CASE_FIXTURE(IsSubtypeFixture, "tables") CHECK(isSubtype(d, a)); CHECK(!isSubtype(a, d)); - CHECK(isSubtype(d, b)); + if (FFlag::DebugLuauDeferredConstraintResolution) + CHECK(!isSubtype(d, b)); // table properties are invariant + else + CHECK(isSubtype(d, b)); CHECK(!isSubtype(b, d)); } @@ -705,6 +720,19 @@ TEST_CASE_FIXTURE(NormalizeFixture, "specific_functions_cannot_be_negated") CHECK(nullptr == toNormalizedType("Not<(boolean) -> boolean>")); } +TEST_CASE_FIXTURE(NormalizeFixture, "trivial_intersection_inhabited") +{ + // this test was used to fix a bug in normalization when working with intersections/unions of the same type. + + TypeId a = arena.addType(FunctionType{builtinTypes->emptyTypePack, builtinTypes->anyTypePack, std::nullopt, false}); + TypeId c = arena.addType(IntersectionType{{a, a}}); + + const NormalizedType* n = normalizer.normalize(c); + REQUIRE(n); + + CHECK(normalizer.isInhabited(n)); +} + TEST_CASE_FIXTURE(NormalizeFixture, "bare_negated_boolean") { // TODO: We don't yet have a way to say number | string | thread | nil | Class | Table | Function @@ -906,4 +934,12 @@ TEST_CASE_FIXTURE(NormalizeFixture, "normalize_is_exactly_number") CHECK(!unionIntersection->isExactlyNumber()); } +TEST_CASE_FIXTURE(NormalizeFixture, "normalize_unknown") +{ + auto nt = toNormalizedType("Not | Not"); + CHECK(nt); + CHECK(nt->isUnknown()); + CHECK(toString(normalizer.typeFromNormal(*nt)) == "unknown"); +} + TEST_SUITE_END(); diff --git a/tests/Subtyping.test.cpp b/tests/Subtyping.test.cpp index 9dc193d8..57455244 100644 --- a/tests/Subtyping.test.cpp +++ b/tests/Subtyping.test.cpp @@ -857,6 +857,15 @@ TEST_CASE_FIXTURE(SubtypeFixture, "Child & ~GrandchildOne numberType); } +TEST_CASE_FIXTURE(SubtypeFixture, "semantic_subtyping_disj") +{ + TypeId subTy = builtinTypes->unknownType; + TypeId superTy = join(negate(builtinTypes->numberType), negate(builtinTypes->stringType)); + SubtypingResult result = isSubtype(subTy, superTy); + CHECK(result.isSubtype); +} + + TEST_CASE_FIXTURE(SubtypeFixture, "t1 where t1 = {trim: (t1) -> string} <: t2 where t2 = {trim: (t2) -> string}") { TypeId t1 = cyclicTable([&](TypeId ty, TableType* tt) { diff --git a/tests/TypeInfer.aliases.test.cpp b/tests/TypeInfer.aliases.test.cpp index de273e98..53772349 100644 --- a/tests/TypeInfer.aliases.test.cpp +++ b/tests/TypeInfer.aliases.test.cpp @@ -185,7 +185,6 @@ TEST_CASE_FIXTURE(Fixture, "mutually_recursive_aliases") LUAU_REQUIRE_NO_ERRORS(result); } -#if 0 TEST_CASE_FIXTURE(Fixture, "generic_aliases") { ScopedFastFlag sff[] = { @@ -200,7 +199,7 @@ TEST_CASE_FIXTURE(Fixture, "generic_aliases") LUAU_REQUIRE_ERROR_COUNT(1, result); const std::string expected = - R"(Type 'bad' could not be converted into 'T'; type bad["v"] (string) is not a subtype of T["v"] (number))"; + R"(Type 'bad' could not be converted into 'T'; at ["v"], string is not a subtype of number)"; CHECK(result.errors[0].location == Location{{4, 31}, {4, 44}}); CHECK_EQ(expected, toString(result.errors[0])); } @@ -220,12 +219,11 @@ TEST_CASE_FIXTURE(Fixture, "dependent_generic_aliases") LUAU_REQUIRE_ERROR_COUNT(1, result); const std::string expected = - R"(Type 'bad' could not be converted into 'U'; type bad["t"]["v"] (string) is not a subtype of U["t"]["v"] (number))"; + R"(Type 'bad' could not be converted into 'U'; at ["t"]["v"], string is not a subtype of number)"; CHECK(result.errors[0].location == Location{{4, 31}, {4, 52}}); CHECK_EQ(expected, toString(result.errors[0])); } -#endif TEST_CASE_FIXTURE(Fixture, "mutually_recursive_generic_aliases") { diff --git a/tests/TypeInfer.cfa.test.cpp b/tests/TypeInfer.cfa.test.cpp index 19700d2c..07652c15 100644 --- a/tests/TypeInfer.cfa.test.cpp +++ b/tests/TypeInfer.cfa.test.cpp @@ -934,7 +934,7 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "prototyping_and_visiting_alias_has_the_same_ { ScopedFastFlag sff{"LuauTinyControlFlowAnalysis", true}; - // In CGB, we walk the block to prototype aliases. We then visit the block in-order, which will resolve the prototype to a real type. + // In CG, we walk the block to prototype aliases. We then visit the block in-order, which will resolve the prototype to a real type. // That second walk assumes that the name occurs in the same `Scope` that the prototype walk had. If we arbitrarily change scope midway // through, we'd invoke UB. CheckResult result = check(R"( diff --git a/tests/TypeInfer.classes.test.cpp b/tests/TypeInfer.classes.test.cpp index 94eec961..a6d3fa5c 100644 --- a/tests/TypeInfer.classes.test.cpp +++ b/tests/TypeInfer.classes.test.cpp @@ -426,8 +426,6 @@ TEST_CASE_FIXTURE(ClassFixture, "unions_of_intersections_of_classes") TEST_CASE_FIXTURE(ClassFixture, "index_instance_property") { - ScopedFastFlag luauAllowIndexClassParameters{"LuauAllowIndexClassParameters", true}; - CheckResult result = check(R"( local function execute(object: BaseClass, name: string) print(object[name]) @@ -440,8 +438,6 @@ TEST_CASE_FIXTURE(ClassFixture, "index_instance_property") TEST_CASE_FIXTURE(ClassFixture, "index_instance_property_nonstrict") { - ScopedFastFlag luauAllowIndexClassParameters{"LuauAllowIndexClassParameters", true}; - CheckResult result = check(R"( --!nonstrict diff --git a/tests/TypeInfer.intersectionTypes.test.cpp b/tests/TypeInfer.intersectionTypes.test.cpp index 18b8ab8a..6892c78f 100644 --- a/tests/TypeInfer.intersectionTypes.test.cpp +++ b/tests/TypeInfer.intersectionTypes.test.cpp @@ -976,7 +976,6 @@ local y = x["Bar"] LUAU_REQUIRE_NO_ERRORS(result); } -#if 0 TEST_CASE_FIXTURE(Fixture, "cli_80596_simplify_degenerate_intersections") { ScopedFastFlag dcr{"DebugLuauDeferredConstraintResolution", true}; @@ -1026,6 +1025,5 @@ TEST_CASE_FIXTURE(Fixture, "cli_80596_simplify_more_realistic_intersections") LUAU_REQUIRE_ERRORS(result); } -#endif TEST_SUITE_END(); diff --git a/tests/TypeInfer.oop.test.cpp b/tests/TypeInfer.oop.test.cpp index a332dee2..7fe4a2c3 100644 --- a/tests/TypeInfer.oop.test.cpp +++ b/tests/TypeInfer.oop.test.cpp @@ -415,7 +415,7 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "promise_type_error_too_complex" * doctest::t // TODO: LTI changes to function call resolution have rendered this test impossibly slow // shared self should fix it, but there may be other mitigations possible as well REQUIRE(!FFlag::DebugLuauDeferredConstraintResolution); - ScopedFastFlag sff{"LuauStacklessTypeClone2", true}; + ScopedFastFlag sff{"LuauStacklessTypeClone3", true}; frontend.options.retainFullTypeGraphs = false; diff --git a/tests/TypeInfer.singletons.test.cpp b/tests/TypeInfer.singletons.test.cpp index 97ee15e1..23314535 100644 --- a/tests/TypeInfer.singletons.test.cpp +++ b/tests/TypeInfer.singletons.test.cpp @@ -350,7 +350,6 @@ Table type 'a' not compatible with type 'Bad' because the former is missing fiel CHECK_EQ(expected, toString(result.errors[0])); } -#if 0 TEST_CASE_FIXTURE(Fixture, "parametric_tagged_union_alias") { ScopedFastFlag sff[] = { @@ -372,7 +371,6 @@ TEST_CASE_FIXTURE(Fixture, "parametric_tagged_union_alias") CHECK(toString(result.errors[0]) == expectedError); } -#endif TEST_CASE_FIXTURE(Fixture, "if_then_else_expression_singleton_options") { diff --git a/tests/TypeInfer.tables.test.cpp b/tests/TypeInfer.tables.test.cpp index d5ae004c..b0a8b98d 100644 --- a/tests/TypeInfer.tables.test.cpp +++ b/tests/TypeInfer.tables.test.cpp @@ -43,7 +43,10 @@ TEST_CASE_FIXTURE(Fixture, "basic") TEST_CASE_FIXTURE(Fixture, "augment_table") { - CheckResult result = check("local t = {} t.foo = 'bar'"); + CheckResult result = check(R"( + local t = {} + t.foo = 'bar' + )"); LUAU_REQUIRE_NO_ERRORS(result); const TableType* tType = get(requireType("t")); @@ -70,6 +73,35 @@ TEST_CASE_FIXTURE(Fixture, "augment_nested_table") CHECK("{ p: { foo: string } }" == toString(requireType("t"), {true})); } +TEST_CASE_FIXTURE(Fixture, "assign_key_at_index_expr") +{ + CheckResult result = check(R"( + function f(t: {[string]: number}) + t["hello"] = 1 + end + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + + // We had a bug where we forgot to record the astType of this particular node. + CHECK("string" == toString(requireTypeAtPosition({2, 19}))); +} + +TEST_CASE_FIXTURE(Fixture, "index_expression_is_checked_against_the_indexer_type") +{ + CheckResult result = check(R"( + function f(t: {[boolean]: number}) + t["hello"] = 15 + end + )"); + + LUAU_REQUIRE_ERROR_COUNT(1, result); + if (FFlag::DebugLuauDeferredConstraintResolution) + CHECK_MESSAGE(get(result.errors[0]), "Expected CannotExtendTable but got " << toString(result.errors[0])); + else + CHECK(get(result.errors[0])); +} + TEST_CASE_FIXTURE(Fixture, "cannot_augment_sealed_table") { CheckResult result = check(R"( diff --git a/tests/TypeInfer.test.cpp b/tests/TypeInfer.test.cpp index a2a28ae5..a1d5e95a 100644 --- a/tests/TypeInfer.test.cpp +++ b/tests/TypeInfer.test.cpp @@ -1453,8 +1453,6 @@ TEST_CASE_FIXTURE(Fixture, "promote_tail_type_packs") */ TEST_CASE_FIXTURE(BuiltinsFixture, "be_sure_to_use_active_txnlog_when_evaluating_a_variadic_overload") { - ScopedFastFlag sff{"LuauVariadicOverloadFix", true}; - CheckResult result = check(R"( local function concat(target: {T}, ...: {T} | T): {T} return (nil :: any) :: {T} diff --git a/tests/TypeInfer.tryUnify.test.cpp b/tests/TypeInfer.tryUnify.test.cpp index cc925cab..9f523ce3 100644 --- a/tests/TypeInfer.tryUnify.test.cpp +++ b/tests/TypeInfer.tryUnify.test.cpp @@ -15,6 +15,9 @@ LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution) struct TryUnifyFixture : Fixture { + // Cannot use `TryUnifyFixture` under DCR. + ScopedFastFlag noDcr{"DebugLuauDeferredConstraintResolution", false}; + TypeArena arena; ScopePtr globalScope{new Scope{arena.addTypePack({TypeId{}})}}; InternalErrorReporter iceHandler; @@ -139,7 +142,7 @@ TEST_CASE_FIXTURE(TryUnifyFixture, "incompatible_tables_are_preserved") CHECK_NE(*getMutable(&tableOne)->props["foo"].type(), *getMutable(&tableTwo)->props["foo"].type()); } -TEST_CASE_FIXTURE(TryUnifyFixture, "uninhabited_intersection_sub_never") +TEST_CASE_FIXTURE(Fixture, "uninhabited_intersection_sub_never") { CheckResult result = check(R"( function f(arg : string & number) : never @@ -149,7 +152,7 @@ TEST_CASE_FIXTURE(TryUnifyFixture, "uninhabited_intersection_sub_never") LUAU_REQUIRE_NO_ERRORS(result); } -TEST_CASE_FIXTURE(TryUnifyFixture, "uninhabited_intersection_sub_anything") +TEST_CASE_FIXTURE(Fixture, "uninhabited_intersection_sub_anything") { CheckResult result = check(R"( function f(arg : string & number) : boolean @@ -159,7 +162,7 @@ TEST_CASE_FIXTURE(TryUnifyFixture, "uninhabited_intersection_sub_anything") LUAU_REQUIRE_NO_ERRORS(result); } -TEST_CASE_FIXTURE(TryUnifyFixture, "uninhabited_table_sub_never") +TEST_CASE_FIXTURE(Fixture, "uninhabited_table_sub_never") { CheckResult result = check(R"( function f(arg : { prop : string & number }) : never @@ -169,7 +172,7 @@ TEST_CASE_FIXTURE(TryUnifyFixture, "uninhabited_table_sub_never") LUAU_REQUIRE_NO_ERRORS(result); } -TEST_CASE_FIXTURE(TryUnifyFixture, "uninhabited_table_sub_anything") +TEST_CASE_FIXTURE(Fixture, "uninhabited_table_sub_anything") { CheckResult result = check(R"( function f(arg : { prop : string & number }) : boolean @@ -179,9 +182,11 @@ TEST_CASE_FIXTURE(TryUnifyFixture, "uninhabited_table_sub_anything") LUAU_REQUIRE_NO_ERRORS(result); } -TEST_CASE_FIXTURE(TryUnifyFixture, "members_of_failed_typepack_unification_are_unified_with_errorType") +TEST_CASE_FIXTURE(Fixture, "members_of_failed_typepack_unification_are_unified_with_errorType") { - ScopedFastFlag sff{"LuauAlwaysCommitInferencesOfFunctionCalls", true}; + ScopedFastFlag sff[] = { + {"LuauAlwaysCommitInferencesOfFunctionCalls", true}, + }; CheckResult result = check(R"( function f(arg: number) end @@ -196,9 +201,11 @@ TEST_CASE_FIXTURE(TryUnifyFixture, "members_of_failed_typepack_unification_are_u CHECK_EQ("*error-type*", toString(requireType("b"))); } -TEST_CASE_FIXTURE(TryUnifyFixture, "result_of_failed_typepack_unification_is_constrained") +TEST_CASE_FIXTURE(Fixture, "result_of_failed_typepack_unification_is_constrained") { - ScopedFastFlag sff{"LuauAlwaysCommitInferencesOfFunctionCalls", true}; + ScopedFastFlag sff[] = { + {"LuauAlwaysCommitInferencesOfFunctionCalls", true}, + }; CheckResult result = check(R"( function f(arg: number) return arg end @@ -214,7 +221,7 @@ TEST_CASE_FIXTURE(TryUnifyFixture, "result_of_failed_typepack_unification_is_con CHECK_EQ("number", toString(requireType("c"))); } -TEST_CASE_FIXTURE(TryUnifyFixture, "typepack_unification_should_trim_free_tails") +TEST_CASE_FIXTURE(Fixture, "typepack_unification_should_trim_free_tails") { CheckResult result = check(R"( --!strict @@ -254,7 +261,7 @@ TEST_CASE_FIXTURE(TryUnifyFixture, "variadic_tails_respect_progress") CHECK(state.errors.empty()); } -TEST_CASE_FIXTURE(TryUnifyFixture, "variadics_should_use_reversed_properly") +TEST_CASE_FIXTURE(Fixture, "variadics_should_use_reversed_properly") { CheckResult result = check(R"( --!strict @@ -373,22 +380,12 @@ TEST_CASE_FIXTURE(TryUnifyFixture, "metatables_unify_against_shape_of_free_table state.log.commit(); REQUIRE_EQ(state.errors.size(), 1); - // clang-format off - const std::string expected = - (FFlag::DebugLuauDeferredConstraintResolution) ? -R"(Type - '{ @metatable { __index: { foo: string } }, {| |} }' -could not be converted into - '{- foo: number -}' -caused by: - Type 'number' could not be converted into 'string')" : -R"(Type + const std::string expected = R"(Type '{ @metatable {| __index: {| foo: string |} |}, { } }' could not be converted into '{- foo: number -}' caused by: Type 'number' could not be converted into 'string')"; - // clang-format on CHECK_EQ(expected, toString(state.errors[0])); } diff --git a/tests/TypePath.test.cpp b/tests/TypePath.test.cpp index 53127c3d..5d4a49bf 100644 --- a/tests/TypePath.test.cpp +++ b/tests/TypePath.test.cpp @@ -93,7 +93,6 @@ TEST_SUITE_BEGIN("TypePathTraversal"); LUAU_REQUIRE_NO_ERRORS(result); \ } while (false); -#if 0 TEST_CASE_FIXTURE(Fixture, "empty_traversal") { CHECK(traverseForType(builtinTypes->numberType, kEmpty, builtinTypes) == builtinTypes->numberType); @@ -475,7 +474,6 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "complex_chains") CHECK(*result == builtinTypes->falseType); } } -#endif TEST_SUITE_END(); // TypePathTraversal diff --git a/tests/conformance/bitwise.lua b/tests/conformance/bitwise.lua index 281ad274..f394dc5b 100644 --- a/tests/conformance/bitwise.lua +++ b/tests/conformance/bitwise.lua @@ -140,6 +140,11 @@ assert(bit32.byteswap(0x10203040) == 0x40302010) assert(bit32.byteswap(0) == 0) assert(bit32.byteswap(-1) == 0xffffffff) +-- bit32.bor(n, 0) must clear top bits +-- we check this obscuring the constant through a global to make sure this gets evaluated fully +high32 = 0x42_1234_5678 +assert(bit32.bor(high32, 0) == 0x1234_5678) + --[[ This test verifies a fix in luauF_replace() where if the 4th parameter was not a number, but the first three are numbers, it will diff --git a/tests/conformance/math.lua b/tests/conformance/math.lua index d285df78..9262f4ea 100644 --- a/tests/conformance/math.lua +++ b/tests/conformance/math.lua @@ -61,6 +61,7 @@ assert(1111111111111111-1111111111111110== 1000.00e-03) -- 1234567890123456 assert(1.1 == '1.'+'.1') assert('1111111111111111'-'1111111111111110' == tonumber" +0.001e+3 \n\t") +assert(10000000000000001 == 10000000000000000) function eq (a,b,limit) if not limit then limit = 10E-10 end diff --git a/tests/conformance/utf8.lua b/tests/conformance/utf8.lua index bfd7a1ac..3314216f 100644 --- a/tests/conformance/utf8.lua +++ b/tests/conformance/utf8.lua @@ -15,20 +15,33 @@ end local justone = "^" .. utf8.charpattern .. "$" +-- 't' is the list of codepoints of 's' +local function checksyntax (s, t) + -- creates a string "return '\u{t[1]}...\u{t[n]}'" + local ts = {"return '"} + for i = 1, #t do ts[i + 1] = string.format("\\u{%x}", t[i]) end + ts[#t + 2] = "'" + ts = table.concat(ts) + -- its execution should result in 's' + assert(assert(loadstring(ts))() == s) +end + assert(not utf8.offset("alo", 5)) assert(not utf8.offset("alo", -4)) -- 'check' makes several tests over the validity of string 's'. -- 't' is the list of codepoints of 's'. -local function check (s, t, nonstrict) - local l = utf8.len(s, 1, -1, nonstrict) +local function check (s, t) + local l = utf8.len(s, 1, -1) assert(#t == l and len(s) == l) assert(utf8.char(table.unpack(t)) == s) -- 't' and 's' are equivalent assert(utf8.offset(s, 0) == 1) + checksyntax(s, t) + -- creates new table with all codepoints of 's' - local t1 = {utf8.codepoint(s, 1, -1, nonstrict)} + local t1 = {utf8.codepoint(s, 1, -1)} assert(#t == #t1) for i = 1, #t do assert(t[i] == t1[i]) end -- 't' is equal to 't1' @@ -38,25 +51,25 @@ local function check (s, t, nonstrict) assert(string.find(string.sub(s, pi, pi1 - 1), justone)) assert(utf8.offset(s, -1, pi1) == pi) assert(utf8.offset(s, i - l - 1) == pi) - assert(pi1 - pi == #utf8.char(utf8.codepoint(s, pi, pi, nonstrict))) + assert(pi1 - pi == #utf8.char(utf8.codepoint(s, pi, pi))) for j = pi, pi1 - 1 do assert(utf8.offset(s, 0, j) == pi) end for j = pi + 1, pi1 - 1 do assert(not utf8.len(s, j)) end - assert(utf8.len(s, pi, pi, nonstrict) == 1) - assert(utf8.len(s, pi, pi1 - 1, nonstrict) == 1) - assert(utf8.len(s, pi, -1, nonstrict) == l - i + 1) - assert(utf8.len(s, pi1, -1, nonstrict) == l - i) - assert(utf8.len(s, 1, pi, nonstrict) == i) + assert(utf8.len(s, pi, pi) == 1) + assert(utf8.len(s, pi, pi1 - 1) == 1) + assert(utf8.len(s, pi, -1) == l - i + 1) + assert(utf8.len(s, pi1, -1) == l - i) + assert(utf8.len(s, 1, pi) == i) end local i = 0 - for p, c in utf8.codes(s, nonstrict) do + for p, c in utf8.codes(s) do i = i + 1 assert(c == t[i] and p == utf8.offset(s, i)) - assert(utf8.codepoint(s, p, p, nonstrict) == c) + assert(utf8.codepoint(s, p, p) == c) end assert(i == #t) @@ -80,9 +93,15 @@ do -- error indication in utf8.len assert(not a and b == p) end check("abc\xE3def", 4) - check("ๆฑ‰ๅญ—\x80", #("ๆฑ‰ๅญ—") + 1) check("\xF4\x9F\xBF", 1) check("\xF4\x9F\xBF\xBF", 1) + -- spurious continuation bytes + check("ๆฑ‰ๅญ—\x80", #("ๆฑ‰ๅญ—") + 1) + check("\x80hello", 1) + check("hel\x80lo", 4) + check("ๆฑ‰ๅญ—\xBF", #("ๆฑ‰ๅญ—") + 1) + check("\xBFhello", 1) + check("hel\xBFlo", 4) end -- errors in utf8.codes @@ -94,7 +113,17 @@ do end) end errorcodes("ab\xff") - -- errorcodes("\u{110000}") + errorcodes("\244\144\128\128") -- "\u{110000}" in Lua 5.4 + errorcodes("in\x80valid") + errorcodes("\xbfinvalid") + errorcodes("ฮฑฮปฯ†\xBFฮฑ") + + -- calling interation function with invalid arguments + local f = utf8.codes("") + assert(f("", 2) == nil) + assert(f("", -1) == nil) + assert(f("", math.mininteger) == nil) + end -- error in initial position for offset @@ -131,16 +160,16 @@ do -- surrogates assert(utf8.codepoint("\u{D7FF}") == 0xD800 - 1) assert(utf8.codepoint("\u{E000}") == 0xDFFF + 1) - assert(utf8.codepoint("\u{D800}", 1, 1, true) == 0xD800) - assert(utf8.codepoint("\u{DFFF}", 1, 1, true) == 0xDFFF) - -- assert(utf8.codepoint("\u{7FFFFFFF}", 1, 1, true) == 0x7FFFFFFF) + assert(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, "\253\191\191\191\191\191") == false) -- 0x7FFFFFFF in Lua 5.4 when called with lax=true end assert(utf8.char() == "") assert(utf8.char(0, 97, 98, 99, 1) == "\0abc\1") assert(utf8.codepoint(utf8.char(0x10FFFF)) == 0x10FFFF) --- assert(utf8.codepoint(utf8.char(0x7FFFFFFF), 1, 1, true) == 2147483647) +assert(pcall(utf8.char, 0x7FFFFFFF) == false) -- valid in Lua 5.4 checkerror("value out of range", utf8.char, 0x7FFFFFFF + 1) checkerror("value out of range", utf8.char, -1) @@ -154,8 +183,8 @@ end invalid("\xF4\x9F\xBF\xBF") -- surrogates --- invalid("\u{D800}") --- invalid("\u{DFFF}") +-- invalid("\u{D800}") TODO: this is an error in Lua 5.4 +-- invalid("\u{DFFF}") TODO: this is an error in Lua 5.4 -- overlong sequences invalid("\xC0\x80") -- zero @@ -182,7 +211,7 @@ s = "\0 \x7F\z s = string.gsub(s, " ", "") check(s, {0,0x7F, 0x80,0x7FF, 0x800,0xFFFF, 0x10000,0x10FFFF}) -x = "ๆ—ฅๆœฌ่ชža-4\0รฉรณ" +local x = "ๆ—ฅๆœฌ่ชža-4\0รฉรณ" check(x, {26085, 26412, 35486, 97, 45, 52, 0, 233, 243}) diff --git a/tests/main.cpp b/tests/main.cpp index 96a6525f..57d4ee7c 100644 --- a/tests/main.cpp +++ b/tests/main.cpp @@ -28,6 +28,7 @@ #endif #include +#include // Indicates if verbose output is enabled; can be overridden via --verbose // Currently, this enables output from 'print', but other verbose output could be enabled eventually. @@ -180,6 +181,70 @@ struct BoostLikeReporter : doctest::IReporter void test_case_skipped(const doctest::TestCaseData&) override {} }; +struct TeamCityReporter : doctest::IReporter +{ + const doctest::TestCaseData* currentTest = nullptr; + + TeamCityReporter(const doctest::ContextOptions& in) {} + + void report_query(const doctest::QueryData&) override {} + + void test_run_start() override {} + + void test_run_end(const doctest::TestRunStats& /*in*/) override {} + + void test_case_start(const doctest::TestCaseData& in) override + { + currentTest = ∈ + printf("##teamcity[testStarted name='%s: %s' captureStandardOutput='true']\n", in.m_test_suite, in.m_name); + } + + // called when a test case is reentered because of unfinished subcases + void test_case_reenter(const doctest::TestCaseData& /*in*/) override {} + + void test_case_end(const doctest::CurrentTestCaseStats& in) override + { + printf("##teamcity[testMetadata testName='%s: %s' name='total_asserts' type='number' value='%d']\n", currentTest->m_test_suite, currentTest->m_name, in.numAssertsCurrentTest); + printf("##teamcity[testMetadata testName='%s: %s' name='failed_asserts' type='number' value='%d']\n", currentTest->m_test_suite, currentTest->m_name, in.numAssertsFailedCurrentTest); + printf("##teamcity[testMetadata testName='%s: %s' name='runtime' type='number' value='%f']\n", currentTest->m_test_suite, currentTest->m_name, in.seconds); + + if (!in.testCaseSuccess) + printf("##teamcity[testFailed name='%s: %s']\n", currentTest->m_test_suite, currentTest->m_name); + + printf("##teamcity[testFinished name='%s: %s']\n", currentTest->m_test_suite, currentTest->m_name); + } + + void test_case_exception(const doctest::TestCaseException& in) override { + printf("##teamcity[testFailed name='%s: %s' message='Unhandled exception' details='%s']\n", currentTest->m_test_suite, currentTest->m_name, in.error_string.c_str()); + } + + void subcase_start(const doctest::SubcaseSignature& /*in*/) override {} + void subcase_end() override {} + + void log_assert(const doctest::AssertData& ad) override { + if(!ad.m_failed) + return; + + if (ad.m_decomp.size()) + fprintf(stderr, "%s(%d): ERROR: %s (%s)\n", ad.m_file, ad.m_line, ad.m_expr, ad.m_decomp.c_str()); + else + fprintf(stderr, "%s(%d): ERROR: %s\n", ad.m_file, ad.m_line, ad.m_expr); + } + + void log_message(const doctest::MessageData& md) override { + const char* severity = (md.m_severity & doctest::assertType::is_warn) ? "WARNING" : "ERROR"; + bool isError = md.m_severity & (doctest::assertType::is_require | doctest::assertType::is_check); + fprintf(isError ? stderr : stdout, "%s(%d): %s: %s\n", md.m_file, md.m_line, severity, md.m_string.c_str()); + } + + void test_case_skipped(const doctest::TestCaseData& in) override + { + printf("##teamcity[testIgnored name='%s: %s' captureStandardOutput='false']\n", in.m_test_suite, in.m_name); + } +}; + +REGISTER_REPORTER("teamcity", 1, TeamCityReporter); + template using FValueResult = std::pair; diff --git a/tools/faillist.txt b/tools/faillist.txt index 46454bbc..ca8112a1 100644 --- a/tools/faillist.txt +++ b/tools/faillist.txt @@ -8,8 +8,6 @@ AstQuery::getDocumentationSymbolAtPosition.table_overloaded_function_prop AutocompleteTest.anonymous_autofilled_generic_on_argument_type_pack_vararg AutocompleteTest.anonymous_autofilled_generic_type_pack_vararg AutocompleteTest.autocomplete_interpolated_string_as_singleton -AutocompleteTest.autocomplete_oop_implicit_self -AutocompleteTest.autocomplete_response_perf1 AutocompleteTest.autocomplete_string_singleton_equality AutocompleteTest.autocomplete_string_singleton_escape AutocompleteTest.autocomplete_string_singletons @@ -230,6 +228,7 @@ IntersectionTypes.table_intersection_write_sealed IntersectionTypes.table_intersection_write_sealed_indirect IntersectionTypes.table_write_sealed_indirect IntersectionTypes.union_saturate_overloaded_functions +isSubtype.any_is_unknown_union_error Linter.DeprecatedApiFenv Linter.FormatStringTyped Linter.TableOperationsIndexer @@ -312,7 +311,7 @@ TableTests.accidentally_checked_prop_in_opposite_branch TableTests.any_when_indexing_into_an_unsealed_table_with_no_indexer_in_nonstrict_mode TableTests.array_factory_function TableTests.call_method -TableTests.cannot_change_type_of_unsealed_table_prop +TableTests.call_method_with_explicit_self_argument TableTests.casting_tables_with_props_into_table_with_indexer2 TableTests.casting_tables_with_props_into_table_with_indexer3 TableTests.casting_tables_with_props_into_table_with_indexer4 @@ -333,9 +332,7 @@ TableTests.cyclic_shifted_tables TableTests.disallow_indexing_into_an_unsealed_table_with_no_indexer_in_strict_mode TableTests.dont_crash_when_setmetatable_does_not_produce_a_metatabletypevar TableTests.dont_extend_unsealed_tables_in_rvalue_position -TableTests.dont_hang_when_trying_to_look_up_in_cyclic_metatable_index TableTests.dont_leak_free_table_props -TableTests.dont_quantify_table_that_belongs_to_outer_scope TableTests.dont_seal_an_unsealed_table_by_passing_it_to_a_function_that_takes_a_sealed_table TableTests.dont_suggest_exact_match_keys TableTests.error_detailed_indexer_key @@ -379,7 +376,6 @@ TableTests.ok_to_set_nil_even_on_non_lvalue_base_expr TableTests.okay_to_add_property_to_unsealed_tables_by_assignment TableTests.okay_to_add_property_to_unsealed_tables_by_function_call TableTests.only_ascribe_synthetic_names_at_module_scope -TableTests.oop_indexer_works TableTests.oop_polymorphic TableTests.open_table_unification_2 TableTests.pass_a_union_of_tables_to_a_function_that_requires_a_table @@ -424,6 +420,8 @@ TableTests.unification_of_unions_in_a_self_referential_type TableTests.unifying_tables_shouldnt_uaf1 TableTests.used_colon_instead_of_dot TableTests.used_dot_instead_of_colon +TableTests.used_dot_instead_of_colon_but_correctly +TableTests.when_augmenting_an_unsealed_table_with_an_indexer_apply_the_correct_scope_to_the_indexer_type TableTests.wrong_assign_does_hit_indexer ToDot.function ToString.exhaustive_toString_of_cyclic_table @@ -557,7 +555,6 @@ TypeInferFunctions.function_is_supertype_of_concrete_functions TypeInferFunctions.function_statement_sealed_table_assignment_through_indexer TypeInferFunctions.generic_packs_are_not_variadic TypeInferFunctions.higher_order_function_2 -TypeInferFunctions.higher_order_function_3 TypeInferFunctions.higher_order_function_4 TypeInferFunctions.improved_function_arg_mismatch_error_nonstrict TypeInferFunctions.improved_function_arg_mismatch_errors @@ -596,10 +593,6 @@ 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_minimized_fragmented_keys_1 -TypeInferLoops.dcr_iteration_minimized_fragmented_keys_2 -TypeInferLoops.dcr_iteration_minimized_fragmented_keys_3 TypeInferLoops.dcr_iteration_on_never_gives_never TypeInferLoops.dcr_xpath_candidates TypeInferLoops.for_in_loop @@ -647,7 +640,6 @@ TypeInferOOP.methods_are_topologically_sorted TypeInferOOP.object_constructor_can_refer_to_method_of_self TypeInferOOP.promise_type_error_too_complex TypeInferOOP.react_style_oo -TypeInferOOP.table_oop TypeInferOperators.add_type_family_works TypeInferOperators.and_binexps_dont_unify TypeInferOperators.cli_38355_recursive_union @@ -694,7 +686,6 @@ TypePackTests.type_alias_type_packs_import TypePackTests.type_packs_with_tails_in_vararg_adjustment TypePackTests.unify_variadic_tails_in_arguments TypePackTests.unify_variadic_tails_in_arguments_free -TypePackTests.variadic_argument_tail TypeSingletons.enums_using_singletons_mismatch TypeSingletons.error_detailed_tagged_union_mismatch_bool TypeSingletons.error_detailed_tagged_union_mismatch_string 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 03/19] 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 04/19] 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 05/19] 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 06/19] 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 07/19] 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 08/19] 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 From 74c532053f829d31bb000c16d3c12732e37dbf3f Mon Sep 17 00:00:00 2001 From: Andy Friesen Date: Fri, 17 Nov 2023 10:46:18 -0800 Subject: [PATCH 09/19] Sync to upstream/release/604 (#1106) New Solver * New algorithm for inferring the types of locals that have no annotations. This algorithm is very conservative by default, but is augmented with some control flow awareness to handle most common scenarios. * Fix bugs in type inference of tables * Improve performance of by switching out standard C++ containers for `DenseHashMap` * Infrastructure to support clearer error messages in strict mode Native Code Generation * Fix a lowering issue with buffer.writeu8 and 0x80-0xff values: A constant argument wasn't truncated to the target type range and that causes an assertion failure in `build.mov`. * Store full lightuserdata value in loop iteration protocol lowering * Add analysis to compute function bytecode distribution * This includes a class to analyze the bytecode operator distribution per function and a CLI tool that produces a JSON report. See the new cmake target `Luau.Bytecode.CLI` --------- Co-authored-by: Aaron Weiss Co-authored-by: Alexander McCord Co-authored-by: Andy Friesen Co-authored-by: Aviral Goel Co-authored-by: Lily Brown Co-authored-by: Vyacheslav Egorov --- Analysis/include/Luau/Constraint.h | 5 + Analysis/include/Luau/ConstraintGenerator.h | 18 +- Analysis/include/Luau/DataFlowGraph.h | 15 +- Analysis/include/Luau/Def.h | 1 + Analysis/include/Luau/Scope.h | 1 + Analysis/include/Luau/Set.h | 68 +++- Analysis/include/Luau/Subtyping.h | 13 +- Analysis/include/Luau/Type.h | 20 +- Analysis/include/Luau/VisitType.h | 9 + Analysis/src/Clone.cpp | 11 + Analysis/src/ConstraintGenerator.cpp | 139 ++++----- Analysis/src/ConstraintSolver.cpp | 143 ++++++--- Analysis/src/DataFlowGraph.cpp | 137 ++++++-- Analysis/src/Def.cpp | 58 ++-- Analysis/src/NonStrictTypeChecker.cpp | 32 +- Analysis/src/Normalize.cpp | 6 + Analysis/src/Scope.cpp | 11 + Analysis/src/Substitution.cpp | 4 + Analysis/src/Subtyping.cpp | 23 +- Analysis/src/ToDot.cpp | 8 + Analysis/src/ToString.cpp | 19 ++ Analysis/src/TypeAttach.cpp | 6 +- Analysis/src/TypeChecker2.cpp | 11 +- CLI/Bytecode.cpp | 295 ++++++++++++++++++ CMakeLists.txt | 5 + CodeGen/include/Luau/BytecodeSummary.h | 81 +++++ CodeGen/src/AssemblyBuilderX64.cpp | 14 +- CodeGen/src/BytecodeSummary.cpp | 71 +++++ CodeGen/src/IrLoweringA64.cpp | 10 +- CodeGen/src/IrLoweringX64.cpp | 47 ++- CodeGen/src/IrTranslation.cpp | 5 +- CodeGen/src/OptimizeConstProp.cpp | 17 +- Common/include/Luau/DenseHash.h | 4 +- Makefile | 15 +- Sources.cmake | 12 + tests/Conformance.test.cpp | 69 ++++ tests/DataFlowGraph.test.cpp | 93 ++++++ tests/Fixture.cpp | 2 +- tests/NonStrictTypeChecker.test.cpp | 169 +++++++++- tests/Set.test.cpp | 39 +++ tests/Subtyping.test.cpp | 60 ++-- tests/ToString.test.cpp | 2 +- tests/TypeInfer.aliases.test.cpp | 6 +- tests/TypeInfer.anyerror.test.cpp | 10 +- tests/TypeInfer.classes.test.cpp | 54 ++-- tests/TypeInfer.generics.test.cpp | 11 +- tests/TypeInfer.intersectionTypes.test.cpp | 2 +- tests/TypeInfer.modules.test.cpp | 24 +- tests/TypeInfer.refinements.test.cpp | 13 +- tests/TypeInfer.singletons.test.cpp | 4 +- tests/TypeInfer.tables.test.cpp | 30 +- tests/TypeInfer.test.cpp | 85 ++--- tests/TypeInfer.typestates.test.cpp | 41 +++ tests/conformance/buffers.lua | 9 + tests/conformance/native.lua | 18 ++ tools/faillist.txt | 23 +- .../heuristicstat.py | 0 tools/test_dcr.py | 11 + 58 files changed, 1736 insertions(+), 373 deletions(-) create mode 100644 CLI/Bytecode.cpp create mode 100644 CodeGen/include/Luau/BytecodeSummary.h create mode 100644 CodeGen/src/BytecodeSummary.cpp rename stats/compiler-stats.py => tools/heuristicstat.py (100%) diff --git a/Analysis/include/Luau/Constraint.h b/Analysis/include/Luau/Constraint.h index ad10ca99..a026fdae 100644 --- a/Analysis/include/Luau/Constraint.h +++ b/Analysis/include/Luau/Constraint.h @@ -190,6 +190,11 @@ struct UnpackConstraint { TypePackId resultPack; TypePackId sourcePack; + + // UnpackConstraint is sometimes used to resolve the types of assignments. + // When this is the case, any LocalTypes in resultPack can have their + // domains extended by the corresponding type from sourcePack. + bool resultIsLValue = false; }; // resultType ~ refine type mode discriminant diff --git a/Analysis/include/Luau/ConstraintGenerator.h b/Analysis/include/Luau/ConstraintGenerator.h index aab31c40..69b0fd20 100644 --- a/Analysis/include/Luau/ConstraintGenerator.h +++ b/Analysis/include/Luau/ConstraintGenerator.h @@ -78,11 +78,13 @@ struct ConstraintGenerator TypeIds types; }; - // During constraint generation, we only populate the Scope::bindings - // property for annotated symbols. Unannotated symbols must be handled in a - // postprocessing step because we have not yet allocated the types that will - // be assigned to those unannotated symbols, so we queue them up here. - std::map inferredBindings; + // Some locals have multiple type states. We wish for Scope::bindings to + // map each local name onto the union of every type that the local can have + // over its lifetime, so we use this map to accumulate the set of types it + // might have. + // + // See the functions recordInferredBinding and fillInInferredBindings. + DenseHashMap inferredBindings{{}}; // Constraints that go straight to the solver. std::vector constraints; @@ -245,8 +247,6 @@ private: std::optional checkLValue(const ScopePtr& scope, AstExprIndexExpr* indexExpr, TypeId assignedTy); TypeId updateProperty(const ScopePtr& scope, AstExpr* expr, TypeId assignedTy); - void updateLValueType(AstExpr* lvalue, TypeId ty); - struct FunctionSignature { // The type of the function. @@ -336,6 +336,10 @@ private: */ void prepopulateGlobalScope(const ScopePtr& globalScope, AstStatBlock* program); + // Record the fact that a particular local has a particular type in at least + // one of its states. + void recordInferredBinding(AstLocal* local, TypeId ty); + void fillInInferredBindings(const ScopePtr& globalScope, AstStatBlock* block); /** Given a function type annotation, return a vector describing the expected types of the calls to the function diff --git a/Analysis/include/Luau/DataFlowGraph.h b/Analysis/include/Luau/DataFlowGraph.h index ab957b89..083e5046 100644 --- a/Analysis/include/Luau/DataFlowGraph.h +++ b/Analysis/include/Luau/DataFlowGraph.h @@ -77,8 +77,11 @@ struct DfgScope DfgScope* parent; bool isLoopScope; - DenseHashMap bindings{Symbol{}}; - DenseHashMap> props{nullptr}; + using Bindings = DenseHashMap; + using Props = DenseHashMap>; + + Bindings bindings{Symbol{}}; + Props props{nullptr}; std::optional lookup(Symbol symbol) const; std::optional lookup(DefId def, const std::string& key) const; @@ -115,7 +118,13 @@ private: std::vector> scopes; DfgScope* childScope(DfgScope* scope, bool isLoopScope = false); - void join(DfgScope* parent, DfgScope* a, DfgScope* b); + + void join(DfgScope* p, DfgScope* a, DfgScope* b); + void joinBindings(DfgScope::Bindings& p, const DfgScope::Bindings& a, const DfgScope::Bindings& b); + void joinProps(DfgScope::Props& p, const DfgScope::Props& a, const DfgScope::Props& b); + + DefId lookup(DfgScope* scope, Symbol symbol); + DefId lookup(DfgScope* scope, DefId def, const std::string& key); ControlFlow visit(DfgScope* scope, AstStatBlock* b); ControlFlow visitBlockWithoutChildScope(DfgScope* scope, AstStatBlock* b); diff --git a/Analysis/include/Luau/Def.h b/Analysis/include/Luau/Def.h index 0a85fdee..e3fec9b6 100644 --- a/Analysis/include/Luau/Def.h +++ b/Analysis/include/Luau/Def.h @@ -80,6 +80,7 @@ struct DefArena DefId freshCell(bool subscripted = false); DefId phi(DefId a, DefId b); + DefId phi(const std::vector& defs); }; } // namespace Luau diff --git a/Analysis/include/Luau/Scope.h b/Analysis/include/Luau/Scope.h index 2360c986..3f2b7355 100644 --- a/Analysis/include/Luau/Scope.h +++ b/Analysis/include/Luau/Scope.h @@ -56,6 +56,7 @@ struct Scope void addBuiltinTypeBinding(const Name& name, const TypeFun& tyFun); std::optional lookup(Symbol sym) const; + std::optional lookupUnrefinedType(DefId def) const; std::optional lookup(DefId def) const; std::optional> lookupEx(DefId def); std::optional> lookupEx(Symbol sym); diff --git a/Analysis/include/Luau/Set.h b/Analysis/include/Luau/Set.h index 5baff136..3f34c325 100644 --- a/Analysis/include/Luau/Set.h +++ b/Analysis/include/Luau/Set.h @@ -15,10 +15,14 @@ template> class Set { private: - DenseHashMap mapping; + using Impl = DenseHashMap; + Impl mapping; size_t entryCount = 0; public: + class const_iterator; + using iterator = const_iterator; + Set(const T& empty_key) : mapping{empty_key} { @@ -83,6 +87,16 @@ public: return count(element) != 0; } + const_iterator begin() const + { + return const_iterator(mapping.begin(), mapping.end()); + } + + const_iterator end() const + { + return const_iterator(mapping.end(), mapping.end()); + } + bool operator==(const Set& there) const { // if the sets are unequal sizes, then they cannot possibly be equal. @@ -100,6 +114,58 @@ public: // otherwise, we've proven the two equal! return true; } + + class const_iterator + { + public: + const_iterator(typename Impl::const_iterator impl, typename Impl::const_iterator end) + : impl(impl) + , end(end) + {} + + const T& operator*() const + { + return impl->first; + } + + const T* operator->() const + { + return &impl->first; + } + + + bool operator==(const const_iterator& other) const + { + return impl == other.impl; + } + + bool operator!=(const const_iterator& other) const + { + return impl != other.impl; + } + + + const_iterator& operator++() + { + do + { + impl++; + } while (impl != end && impl->second == false); + // keep iterating past pairs where the value is `false` + + return *this; + } + + const_iterator operator++(int) + { + const_iterator res = *this; + ++*this; + return res; + } + private: + typename Impl::const_iterator impl; + typename Impl::const_iterator end; + }; }; } // namespace Luau diff --git a/Analysis/include/Luau/Subtyping.h b/Analysis/include/Luau/Subtyping.h index cb2d48dd..926ffc9c 100644 --- a/Analysis/include/Luau/Subtyping.h +++ b/Analysis/include/Luau/Subtyping.h @@ -27,10 +27,19 @@ struct TypeArena; struct Scope; struct TableIndexer; +enum class SubtypingVariance +{ + // Used for an empty key. Should never appear in actual code. + Invalid, + Covariant, + Invariant, +}; + struct SubtypingReasoning { Path subPath; Path superPath; + SubtypingVariance variance = SubtypingVariance::Covariant; bool operator==(const SubtypingReasoning& other) const; }; @@ -49,7 +58,8 @@ struct SubtypingResult /// The reason for isSubtype to be false. May not be present even if /// isSubtype is false, depending on the input types. - DenseHashSet reasoning{SubtypingReasoning{}}; + DenseHashSet reasoning{ + SubtypingReasoning{TypePath::kEmpty, TypePath::kEmpty, SubtypingVariance::Invalid}}; SubtypingResult& andAlso(const SubtypingResult& other); SubtypingResult& orElse(const SubtypingResult& other); @@ -59,6 +69,7 @@ struct SubtypingResult SubtypingResult& withBothPath(TypePath::Path path); SubtypingResult& withSubPath(TypePath::Path path); SubtypingResult& withSuperPath(TypePath::Path path); + SubtypingResult& withVariance(SubtypingVariance variance); // Only negates the `isSubtype`. static SubtypingResult negate(const SubtypingResult& result); diff --git a/Analysis/include/Luau/Type.h b/Analysis/include/Luau/Type.h index 51d2ded1..70494685 100644 --- a/Analysis/include/Luau/Type.h +++ b/Analysis/include/Luau/Type.h @@ -86,6 +86,24 @@ struct FreeType TypeId upperBound = nullptr; }; +/** A type that tracks the domain of a local variable. + * + * We consider each local's domain to be the union of all types assigned to it. + * We accomplish this with LocalType. Each time we dispatch an assignment to a + * local, we accumulate this union and decrement blockCount. + * + * When blockCount reaches 0, we can consider the LocalType to be "fully baked" + * and replace it with the union we've built. + */ +struct LocalType +{ + TypeId domain; + int blockCount = 0; + + // Used for debugging + std::string name; +}; + struct GenericType { // By default, generics are global, with a synthetic name @@ -623,7 +641,7 @@ struct NegationType using ErrorType = Unifiable::Error; using TypeVariant = - Unifiable::Variant; struct Type final diff --git a/Analysis/include/Luau/VisitType.h b/Analysis/include/Luau/VisitType.h index ea0acd2b..6e1fea6a 100644 --- a/Analysis/include/Luau/VisitType.h +++ b/Analysis/include/Luau/VisitType.h @@ -97,6 +97,10 @@ struct GenericTypeVisitor { return visit(ty); } + virtual bool visit(TypeId ty, const LocalType& ftv) + { + return visit(ty); + } virtual bool visit(TypeId ty, const GenericType& gtv) { return visit(ty); @@ -241,6 +245,11 @@ struct GenericTypeVisitor else visit(ty, *ftv); } + else if (auto lt = get(ty)) + { + if (visit(ty, *lt)) + traverse(lt->domain); + } else if (auto gtv = get(ty)) visit(ty, *gtv); else if (auto etv = get(ty)) diff --git a/Analysis/src/Clone.cpp b/Analysis/src/Clone.cpp index 1b97bb89..5fe9e787 100644 --- a/Analysis/src/Clone.cpp +++ b/Analysis/src/Clone.cpp @@ -261,6 +261,11 @@ private: t->upperBound = shallowClone(t->upperBound); } + void cloneChildren(LocalType* t) + { + t->domain = shallowClone(t->domain); + } + void cloneChildren(GenericType* t) { // TOOD: clone upper bounds. @@ -504,6 +509,7 @@ struct TypeCloner void defaultClone(const T& t); void operator()(const FreeType& t); + void operator()(const LocalType& t); void operator()(const GenericType& t); void operator()(const BoundType& t); void operator()(const ErrorType& t); @@ -631,6 +637,11 @@ void TypeCloner::operator()(const FreeType& t) defaultClone(t); } +void TypeCloner::operator()(const LocalType& t) +{ + defaultClone(t); +} + void TypeCloner::operator()(const GenericType& t) { defaultClone(t); diff --git a/Analysis/src/ConstraintGenerator.cpp b/Analysis/src/ConstraintGenerator.cpp index 15e64c92..12e4e7da 100644 --- a/Analysis/src/ConstraintGenerator.cpp +++ b/Analysis/src/ConstraintGenerator.cpp @@ -205,33 +205,6 @@ 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)) @@ -243,7 +216,7 @@ std::optional ConstraintGenerator::lookup(Scope* scope, DefId def) TypeId res = builtinTypes->neverType; - for (DefId operand : flatten(phi)) + for (DefId operand : phi->operands) { // `scope->lookup(operand)` may return nothing because it could be a phi node of globals, but one of // the operand of that global has never been assigned a type, and so it should be an error. @@ -621,8 +594,12 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStat* stat) ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatLocal* statLocal) { - std::vector> varTypes; - varTypes.reserve(statLocal->vars.size); + std::vector annotatedTypes; + annotatedTypes.reserve(statLocal->vars.size); + bool hasAnnotation = false; + + std::vector> expectedTypes; + expectedTypes.reserve(statLocal->vars.size); std::vector assignees; assignees.reserve(statLocal->vars.size); @@ -635,7 +612,8 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatLocal* stat { const Location location = local->location; - TypeId assignee = arena->addType(BlockedType{}); + TypeId assignee = arena->addType(LocalType{builtinTypes->neverType, /* blockCount */ 1, local->name.value}); + assignees.push_back(assignee); if (!firstValueType) @@ -643,16 +621,21 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatLocal* stat if (local->annotation) { + hasAnnotation = true; TypeId annotationTy = resolveType(scope, local->annotation, /* inTypeArguments */ false); - varTypes.push_back(annotationTy); - - addConstraint(scope, local->location, SubtypeConstraint{assignee, annotationTy}); + annotatedTypes.push_back(annotationTy); + expectedTypes.push_back(annotationTy); scope->bindings[local] = Binding{annotationTy, location}; } else { - varTypes.push_back(std::nullopt); + // annotatedTypes must contain one type per local. If a particular + // local has no annotation at, assume the most conservative thing. + annotatedTypes.push_back(builtinTypes->unknownType); + + expectedTypes.push_back(std::nullopt); + scope->bindings[local] = Binding{builtinTypes->unknownType, location}; inferredBindings[local] = {scope.get(), location, {assignee}}; } @@ -661,8 +644,12 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatLocal* stat scope->lvalueTypes[def] = assignee; } - TypePackId resultPack = checkPack(scope, statLocal->values, varTypes).tp; - addConstraint(scope, statLocal->location, UnpackConstraint{arena->addTypePack(std::move(assignees)), resultPack}); + TypePackId resultPack = checkPack(scope, statLocal->values, expectedTypes).tp; + addConstraint(scope, statLocal->location, UnpackConstraint{arena->addTypePack(std::move(assignees)), resultPack, /*resultIsLValue*/ true}); + + // Types must flow between whatever annotations were provided and the rhs expression. + if (hasAnnotation) + addConstraint(scope, statLocal->location, PackSubtypeConstraint{resultPack, arena->addTypePack(std::move(annotatedTypes))}); if (statLocal->vars.size == 1 && statLocal->values.size == 1 && firstValueType && scope.get() == rootScope) { @@ -1006,26 +993,22 @@ static void bindFreeType(TypeId a, TypeId b) ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatAssign* assign) { - std::vector> expectedTypes; - expectedTypes.reserve(assign->vars.size); - std::vector assignees; assignees.reserve(assign->vars.size); for (AstExpr* lvalue : assign->vars) { TypeId assignee = arena->addType(BlockedType{}); - assignees.push_back(assignee); checkLValue(scope, lvalue, assignee); + assignees.push_back(assignee); DefId def = dfg->getDef(lvalue); scope->lvalueTypes[def] = assignee; - updateLValueType(lvalue, assignee); } - TypePackId resultPack = checkPack(scope, assign->values, expectedTypes).tp; - addConstraint(scope, assign->location, UnpackConstraint{arena->addTypePack(std::move(assignees)), resultPack}); + TypePackId resultPack = checkPack(scope, assign->values).tp; + addConstraint(scope, assign->location, UnpackConstraint{arena->addTypePack(std::move(assignees)), resultPack, /*resultIsLValue*/ true}); return ControlFlow::None; } @@ -1545,8 +1528,7 @@ InferencePack ConstraintGenerator::checkPack(const ScopePtr& scope, AstExprCall* scope->lvalueTypes[def] = resultTy; // TODO: typestates: track this as an assignment scope->rvalueRefinements[def] = resultTy; // TODO: typestates: track this as an assignment - if (auto it = inferredBindings.find(targetLocal->local); it != inferredBindings.end()) - it->second.types.insert(resultTy); + recordInferredBinding(targetLocal->local, resultTy); } return InferencePack{arena->addTypePack({resultTy}), {refinementArena.variadic(returnRefinements)}}; @@ -1723,8 +1705,8 @@ Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprLocal* local) if (maybeTy) { TypeId ty = follow(*maybeTy); - if (auto it = inferredBindings.find(local->local); it != inferredBindings.end()) - it->second.types.insert(ty); + + recordInferredBinding(local->local, ty); return Inference{ty, refinementArena.proposition(key, builtinTypes->truthyType)}; } @@ -2210,23 +2192,35 @@ std::optional ConstraintGenerator::checkLValue(const ScopePtr& scope, As std::optional ConstraintGenerator::checkLValue(const ScopePtr& scope, AstExprLocal* local, TypeId assignedTy) { - /* - * The caller of this method uses the returned type to emit the proper - * SubtypeConstraint. - * - * At this point during constraint generation, the binding table is only - * populated by symbols that have type annotations. - * - * If this local has an interesting type annotation, it is important that we - * return that and constrain the assigned type. - */ std::optional annotatedTy = scope->lookup(local->local); + LUAU_ASSERT(annotatedTy); if (annotatedTy) addConstraint(scope, local->location, SubtypeConstraint{assignedTy, *annotatedTy}); - else if (auto it = inferredBindings.find(local->local); it == inferredBindings.end()) - ice->ice("Cannot find AstLocal* in either Scope::bindings or inferredBindings?"); - return annotatedTy; + const DefId defId = dfg->getDef(local); + std::optional ty = scope->lookupUnrefinedType(defId); + + if (ty) + { + if (auto lt = getMutable(*ty)) + ++lt->blockCount; + } + else + { + ty = arena->addType(LocalType{builtinTypes->neverType, /* blockCount */ 1, local->local->name.value}); + + scope->lvalueTypes[defId] = *ty; + } + + addConstraint(scope, local->location, UnpackConstraint{ + arena->addTypePack({*ty}), + arena->addTypePack({assignedTy}), + /*resultIsLValue*/ true + }); + + recordInferredBinding(local->local, *ty); + + return ty; } std::optional ConstraintGenerator::checkLValue(const ScopePtr& scope, AstExprGlobal* global, TypeId assignedTy) @@ -2379,15 +2373,6 @@ TypeId ConstraintGenerator::updateProperty(const ScopePtr& scope, AstExpr* expr, return assignedTy; } -void ConstraintGenerator::updateLValueType(AstExpr* lvalue, TypeId ty) -{ - if (auto local = lvalue->as()) - { - if (auto it = inferredBindings.find(local->local); it != inferredBindings.end()) - it->second.types.insert(ty); - } -} - Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprTable* expr, std::optional expectedType) { const bool expectedTypeIsFree = expectedType && get(follow(*expectedType)); @@ -2611,13 +2596,7 @@ ConstraintGenerator::FunctionSignature ConstraintGenerator::checkFunctionSignatu argTypes.push_back(argTy); argNames.emplace_back(FunctionArgument{local->name.value, local->location}); - if (local->annotation) - signatureScope->bindings[local] = Binding{argTy, local->location}; - else - { - signatureScope->bindings[local] = Binding{builtinTypes->neverType, local->location}; - inferredBindings[local] = {signatureScope.get(), {}}; - } + signatureScope->bindings[local] = Binding{argTy, local->location}; DefId def = dfg->getDef(local); signatureScope->lvalueTypes[def] = argTy; @@ -3125,6 +3104,12 @@ void ConstraintGenerator::prepopulateGlobalScope(const ScopePtr& globalScope, As program->visit(&gp); } +void ConstraintGenerator::recordInferredBinding(AstLocal* local, TypeId ty) +{ + if (InferredBinding* ib = inferredBindings.find(local)) + ib->types.insert(ty); +} + void ConstraintGenerator::fillInInferredBindings(const ScopePtr& globalScope, AstStatBlock* block) { for (const auto& [symbol, p] : inferredBindings) diff --git a/Analysis/src/ConstraintSolver.cpp b/Analysis/src/ConstraintSolver.cpp index 482997c4..de2f566d 100644 --- a/Analysis/src/ConstraintSolver.cpp +++ b/Analysis/src/ConstraintSolver.cpp @@ -993,6 +993,27 @@ bool ConstraintSolver::tryDispatch(const FunctionCallConstraint& c, NotNull std::optional { auto it = begin(t); auto endIt = end(t); @@ -1018,10 +1039,12 @@ bool ConstraintSolver::tryDispatch(const FunctionCallConstraint& c, NotNull callMm = findMetatableEntry(builtinTypes, errors, fn, "__call", constraint->location)) { - auto [head, tail] = flatten(c.argsPack); - head.insert(head.begin(), fn); + argsHead.insert(argsHead.begin(), fn); - argsPack = arena->addTypePack(TypePack{std::move(head), tail}); + if (argsTail && isBlocked(*argsTail)) + return block(*argsTail, constraint); + + argsPack = arena->addTypePack(TypePack{std::move(argsHead), argsTail}); fn = follow(*callMm); asMutable(c.result)->ty.emplace(constraint->scope); } @@ -1136,23 +1159,14 @@ bool ConstraintSolver::tryDispatch(const PrimitiveTypeConstraint& c, NotNull constraint) { - TypeId subjectType = follow(c.subjectType); + const TypeId subjectType = follow(c.subjectType); + const TypeId resultType = follow(c.resultType); - LUAU_ASSERT(get(c.resultType)); + LUAU_ASSERT(get(resultType)); if (isBlocked(subjectType) || get(subjectType)) return block(subjectType, constraint); - if (get(subjectType)) - { - TableType& ttv = asMutable(subjectType)->ty.emplace(TableState::Free, TypeLevel{}, constraint->scope); - ttv.props[c.prop] = Property{c.resultType}; - TypeId res = freshType(arena, builtinTypes, constraint->scope); - asMutable(c.resultType)->ty.emplace(res); - unblock(c.resultType, constraint->location); - return true; - } - auto [blocked, result] = lookupTableProp(subjectType, c.prop, c.suppressSimplification); if (!blocked.empty()) { @@ -1162,8 +1176,8 @@ bool ConstraintSolver::tryDispatch(const HasPropConstraint& c, NotNullanyType), c.subjectType, constraint->location); - unblock(c.resultType, constraint->location); + bindBlockedType(resultType, result.value_or(builtinTypes->anyType), c.subjectType, constraint->location); + unblock(resultType, constraint->location); return true; } @@ -1438,32 +1452,57 @@ bool ConstraintSolver::tryDispatch(const UnpackConstraint& c, NotNull= srcPack.head.size()) break; - TypeId srcTy = follow(srcPack.head[i]); - if (isBlocked(*destIter)) + TypeId srcTy = follow(srcPack.head[i]); + TypeId resultTy = follow(*resultIter); + + if (resultTy) { - if (follow(srcTy) == *destIter) + if (auto lt = getMutable(resultTy); c.resultIsLValue && lt) { - // Cyclic type dependency. (????) - TypeId f = freshType(arena, builtinTypes, constraint->scope); - asMutable(*destIter)->ty.emplace(f); + lt->domain = simplifyUnion(builtinTypes, arena, lt->domain, srcTy).result; + LUAU_ASSERT(lt->blockCount > 0); + --lt->blockCount; + + LUAU_ASSERT(0 <= lt->blockCount); + + if (0 == lt->blockCount) + asMutable(resultTy)->ty.emplace(lt->domain); + } + else if (get(resultTy)) + { + if (follow(srcTy) == resultTy) + { + // It is sometimes the case that we find that a blocked type + // is only blocked on itself. This doesn't actually + // constitute any meaningful constraint, so we replace it + // with a free type. + TypeId f = freshType(arena, builtinTypes, constraint->scope); + asMutable(resultTy)->ty.emplace(f); + } + else + asMutable(resultTy)->ty.emplace(srcTy); } else - asMutable(*destIter)->ty.emplace(srcTy); - unblock(*destIter, constraint->location); + { + LUAU_ASSERT(c.resultIsLValue); + unify(constraint->scope, constraint->location, resultTy, srcTy); + } + + unblock(resultTy, constraint->location); } else - unify(constraint->scope, constraint->location, *destIter, srcTy); + unify(constraint->scope, constraint->location, resultTy, srcTy); - ++destIter; + ++resultIter; ++i; } @@ -1471,15 +1510,25 @@ bool ConstraintSolver::tryDispatch(const UnpackConstraint& c, NotNull(resultTy); c.resultIsLValue && lt) { - asMutable(*destIter)->ty.emplace(builtinTypes->nilType); - unblock(*destIter, constraint->location); + lt->domain = simplifyUnion(builtinTypes, arena, lt->domain, builtinTypes->nilType).result; + LUAU_ASSERT(0 <= lt->blockCount); + --lt->blockCount; + + if (0 == lt->blockCount) + asMutable(resultTy)->ty.emplace(lt->domain); + } + else if (get(*resultIter) || get(*resultIter)) + { + asMutable(*resultIter)->ty.emplace(builtinTypes->nilType); + unblock(*resultIter, constraint->location); } - ++destIter; + ++resultIter; } return true; @@ -1999,14 +2048,23 @@ std::pair, std::optional> ConstraintSolver::lookupTa } else if (auto ft = get(subjectType)) { - Scope* scope = ft->scope; + const TypeId upperBound = follow(ft->upperBound); - TableType* tt = &asMutable(subjectType)->ty.emplace(); - tt->state = TableState::Free; - tt->scope = scope; + if (get(upperBound)) + return lookupTableProp(upperBound, propName, suppressSimplification, seen); + + // TODO: The upper bound could be an intersection that contains suitable tables or classes. + + NotNull scope{ft->scope}; + + const TypeId newUpperBound = arena->addType(TableType{TableState::Free, TypeLevel{}, scope}); + TableType* tt = getMutable(newUpperBound); + LUAU_ASSERT(tt); TypeId propType = freshType(arena, builtinTypes, scope); tt->props[propName] = Property{propType}; + unify(scope, Location{}, subjectType, newUpperBound); + return {{}, propType}; } else if (auto utv = get(subjectType)) @@ -2298,7 +2356,12 @@ void ConstraintSolver::unblock(const std::vector& packs, Location lo bool ConstraintSolver::isBlocked(TypeId ty) { - return nullptr != get(follow(ty)) || nullptr != get(follow(ty)); + ty = follow(ty); + + if (auto lt = get(ty)) + return lt->blockCount > 0; + + return nullptr != get(ty) || nullptr != get(ty); } bool ConstraintSolver::isBlocked(TypePackId tp) diff --git a/Analysis/src/DataFlowGraph.cpp b/Analysis/src/DataFlowGraph.cpp index 60d95986..bdefd7f0 100644 --- a/Analysis/src/DataFlowGraph.cpp +++ b/Analysis/src/DataFlowGraph.cpp @@ -161,25 +161,109 @@ DfgScope* DataFlowGraphBuilder::childScope(DfgScope* scope, bool isLoopScope) void DataFlowGraphBuilder::join(DfgScope* p, DfgScope* a, DfgScope* b) { - // TODO TODO FIXME IMPLEMENT JOIN LOGIC FOR PROPERTIES + joinBindings(p->bindings, a->bindings, b->bindings); + joinProps(p->props, a->props, b->props); +} - for (const auto& [sym, def1] : a->bindings) +void DataFlowGraphBuilder::joinBindings(DfgScope::Bindings& p, const DfgScope::Bindings& a, const DfgScope::Bindings& b) +{ + for (const auto& [sym, def1] : a) { - 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}); + if (auto def2 = b.find(sym)) + p[sym] = defArena->phi(NotNull{def1}, NotNull{*def2}); + else if (auto def2 = p.find(sym)) + p[sym] = defArena->phi(NotNull{def1}, NotNull{*def2}); } - for (const auto& [sym, def1] : b->bindings) + for (const auto& [sym, def1] : b) { - if (a->bindings.find(sym)) + if (auto def2 = p.find(sym)) + p[sym] = defArena->phi(NotNull{def1}, NotNull{*def2}); + } +} + +void DataFlowGraphBuilder::joinProps(DfgScope::Props& p, const DfgScope::Props& a, const DfgScope::Props& b) +{ + auto phinodify = [this](auto& p, const auto& a, const auto& b) mutable { + for (const auto& [k, defA] : a) + { + if (auto it = b.find(k); it != b.end()) + p[k] = defArena->phi(NotNull{it->second}, NotNull{defA}); + else if (auto it = p.find(k); it != p.end()) + p[k] = defArena->phi(NotNull{it->second}, NotNull{defA}); + else + p[k] = defA; + } + + for (const auto& [k, defB] : b) + { + if (auto it = a.find(k); it != a.end()) + continue; + else if (auto it = p.find(k); it != p.end()) + p[k] = defArena->phi(NotNull{it->second}, NotNull{defB}); + else + p[k] = defB; + } + }; + + for (const auto& [def, a1] : a) + { + p.try_insert(def, {}); + if (auto a2 = b.find(def)) + phinodify(p[def], a1, *a2); + else if (auto a2 = p.find(def)) + phinodify(p[def], a1, *a2); + } + + for (const auto& [def, a1] : b) + { + p.try_insert(def, {}); + if (a.find(def)) continue; - else if (auto def2 = p->bindings.find(sym)) - p->bindings[sym] = defArena->phi(NotNull{def1}, NotNull{*def2}); + else if (auto a2 = p.find(def)) + phinodify(p[def], a1, *a2); } } +DefId DataFlowGraphBuilder::lookup(DfgScope* scope, Symbol symbol) +{ + if (auto found = scope->lookup(symbol)) + return *found; + else + { + DefId result = defArena->freshCell(); + if (symbol.local) + scope->bindings[symbol] = result; + else + moduleScope->bindings[symbol] = result; + return result; + } +} + +DefId DataFlowGraphBuilder::lookup(DfgScope* scope, DefId def, const std::string& key) +{ + if (auto found = scope->lookup(def, key)) + return *found; + else if (auto phi = get(def)) + { + std::vector defs; + for (DefId operand : phi->operands) + defs.push_back(lookup(scope, operand, key)); + + DefId result = defArena->phi(defs); + scope->props[def][key] = result; + return result; + } + else if (get(def)) + { + DefId result = defArena->freshCell(); + scope->props[def][key] = result; + return result; + } + else + handle->ice("Inexhaustive lookup cases in DataFlowGraphBuilder::lookup"); +} + ControlFlow DataFlowGraphBuilder::visit(DfgScope* scope, AstStatBlock* b) { DfgScope* child = childScope(scope); @@ -585,6 +669,7 @@ DataFlowResult DataFlowGraphBuilder::visitExpr(DfgScope* scope, AstExprGroup* gr DataFlowResult DataFlowGraphBuilder::visitExpr(DfgScope* scope, AstExprLocal* l) { + // DfgScope::lookup is intentional here: we want to be able to ice. if (auto def = scope->lookup(l->local)) { const RefinementKey* key = keyArena->leaf(*def); @@ -596,11 +681,7 @@ DataFlowResult DataFlowGraphBuilder::visitExpr(DfgScope* scope, AstExprLocal* l) DataFlowResult DataFlowGraphBuilder::visitExpr(DfgScope* scope, AstExprGlobal* g) { - if (auto def = scope->lookup(g->name)) - return {*def, keyArena->leaf(*def)}; - - DefId def = defArena->freshCell(); - moduleScope->bindings[g->name] = def; + DefId def = lookup(scope, g->name); return {def, keyArena->leaf(def)}; } @@ -619,14 +700,9 @@ DataFlowResult DataFlowGraphBuilder::visitExpr(DfgScope* scope, AstExprIndexName auto [parentDef, parentKey] = visitExpr(scope, i->expr); std::string index = i->index.value; - 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)}; - } + + DefId def = lookup(scope, parentDef, index); + return {def, keyArena->node(parentKey, def, index)}; } DataFlowResult DataFlowGraphBuilder::visitExpr(DfgScope* scope, AstExprIndexExpr* i) @@ -637,14 +713,9 @@ DataFlowResult DataFlowGraphBuilder::visitExpr(DfgScope* scope, AstExprIndexExpr if (auto string = i->index->as()) { std::string index{string->value.data, string->value.size}; - 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)}; - } + + DefId def = lookup(scope, parentDef, index); + return {def, keyArena->node(parentKey, def, index)}; } return {defArena->freshCell(/* subscripted= */true), nullptr}; @@ -795,8 +866,8 @@ void DataFlowGraphBuilder::visitLValue(DfgScope* scope, AstExprGlobal* g, DefId // We need to keep the previous def around for a compound assignment. if (isCompoundAssignment) { - if (auto def = scope->lookup(g->name)) - graph.compoundAssignDefs[g] = *def; + DefId def = lookup(scope, g->name); + graph.compoundAssignDefs[g] = def; } // In order to avoid alias tracking, we need to clip the reference to the parent def. diff --git a/Analysis/src/Def.cpp b/Analysis/src/Def.cpp index fdbc089f..2b3bbeac 100644 --- a/Analysis/src/Def.cpp +++ b/Analysis/src/Def.cpp @@ -1,8 +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 "Luau/Common.h" + +#include #include namespace Luau @@ -13,27 +14,9 @@ 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}; - - 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; + return std::any_of(phi->operands.begin(), phi->operands.end(), containsSubscriptedDefinition); + else + return false; } DefId DefArena::freshCell(bool subscripted) @@ -41,12 +24,35 @@ DefId DefArena::freshCell(bool subscripted) return NotNull{allocator.allocate(Def{Cell{subscripted}})}; } +static void collectOperands(DefId def, std::vector& operands) +{ + if (std::find(operands.begin(), operands.end(), def) != operands.end()) + return; + else if (get(def)) + operands.push_back(def); + else if (auto phi = get(def)) + { + for (const Def* operand : phi->operands) + collectOperands(NotNull{operand}, operands); + } +} + DefId DefArena::phi(DefId a, DefId b) { - if (a == b) - return a; + return phi({a, b}); +} + +DefId DefArena::phi(const std::vector& defs) +{ + std::vector operands; + for (DefId operand : defs) + collectOperands(operand, operands); + + // There's no need to allocate a Phi node for a singleton set. + if (operands.size() == 1) + return operands[0]; else - return NotNull{allocator.allocate(Def{Phi{{a, b}}})}; + return NotNull{allocator.allocate(Def{Phi{std::move(operands)}})}; } } // namespace Luau diff --git a/Analysis/src/NonStrictTypeChecker.cpp b/Analysis/src/NonStrictTypeChecker.cpp index 5ff782ea..451fa8f6 100644 --- a/Analysis/src/NonStrictTypeChecker.cpp +++ b/Analysis/src/NonStrictTypeChecker.cpp @@ -84,7 +84,7 @@ struct NonStrictContext for (auto [def, rightTy] : right.context) { - if (!right.find(def).has_value()) + if (!left.find(def).has_value()) disj.context[def] = rightTy; } @@ -270,18 +270,24 @@ struct NonStrictTypeChecker NonStrictContext visit(AstStatBlock* block) { auto StackPusher = pushStack(block); + NonStrictContext ctx; for (AstStat* statement : block->body) - visit(statement); - return {}; + ctx = NonStrictContext::disjunction(builtinTypes, NotNull{&arena}, ctx, visit(statement)); + return ctx; } NonStrictContext visit(AstStatIf* ifStatement) { 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)); + NonStrictContext branchContext; + // If there is no else branch, don't bother generating warnings for the then branch - we can't prove there is an error + if (ifStatement->elsebody) + { + NonStrictContext thenBody = visit(ifStatement->thenbody); + NonStrictContext elseBody = visit(ifStatement->elsebody); + branchContext = NonStrictContext::conjunction(builtinTypes, NotNull{&arena}, thenBody, elseBody); + } + return NonStrictContext::disjunction(builtinTypes, NotNull{&arena}, condB, branchContext); } NonStrictContext visit(AstStatWhile* whileStatement) @@ -316,6 +322,8 @@ struct NonStrictTypeChecker NonStrictContext visit(AstStatLocal* local) { + for (AstExpr* rhs : local->values) + visit(rhs); return {}; } @@ -341,12 +349,12 @@ struct NonStrictTypeChecker NonStrictContext visit(AstStatFunction* statFn) { - return {}; + return visit(statFn->func); } NonStrictContext visit(AstStatLocalFunction* localFn) { - return {}; + return visit(localFn->func); } NonStrictContext visit(AstStatTypeAlias* typeAlias) @@ -530,7 +538,7 @@ struct NonStrictTypeChecker NonStrictContext visit(AstExprFunction* exprFn) { auto pusher = pushStack(exprFn); - return {}; + return visit(exprFn->body); } NonStrictContext visit(AstExprTable* table) @@ -589,10 +597,6 @@ struct NonStrictTypeChecker SubtypingResult r = subtyping.isSubtype(actualType, *contextTy); if (r.normalizationTooComplex) reportError(NormalizationTooComplex{}, fragment->location); - - if (!r.isSubtype && !r.isErrorSuppressing) - reportError(TypeMismatch{actualType, *contextTy}, fragment->location); - if (r.isSubtype) return {actualType}; } diff --git a/Analysis/src/Normalize.cpp b/Analysis/src/Normalize.cpp index 9f14b355..3c961047 100644 --- a/Analysis/src/Normalize.cpp +++ b/Analysis/src/Normalize.cpp @@ -1623,6 +1623,12 @@ bool Normalizer::unionNormalWithTy(NormalizedType& here, TypeId there, SetunknownType; here.tyvars.insert_or_assign(there, std::make_unique(std::move(inter))); } + else if (auto lt = get(there)) + { + // FIXME? This is somewhat questionable. + // Maybe we should assert because this should never happen? + unionNormalWithTy(here, lt->domain, seenSetTypes, ignoreSmallerTyvars); + } else if (get(there)) unionFunctionsWithFunction(here.functions, there); else if (get(there) || get(there)) diff --git a/Analysis/src/Scope.cpp b/Analysis/src/Scope.cpp index 6beffc2c..a3182c0a 100644 --- a/Analysis/src/Scope.cpp +++ b/Analysis/src/Scope.cpp @@ -72,6 +72,17 @@ std::optional> Scope::lookupEx(Symbol sym) } } +std::optional Scope::lookupUnrefinedType(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; +} + std::optional Scope::lookup(DefId def) const { for (const Scope* current = this; current; current = current->parent.get()) diff --git a/Analysis/src/Substitution.cpp b/Analysis/src/Substitution.cpp index 176f1506..2f4e9611 100644 --- a/Analysis/src/Substitution.cpp +++ b/Analysis/src/Substitution.cpp @@ -19,8 +19,12 @@ static TypeId shallowClone(TypeId ty, TypeArena& dest, const TxnLog* log, bool a auto go = [ty, &dest, alwaysClone](auto&& a) { using T = std::decay_t; + // The pointer identities of free and local types is very important. + // We decline to copy them. if constexpr (std::is_same_v) return ty; + else if constexpr (std::is_same_v) + return ty; else if constexpr (std::is_same_v) { // This should never happen, but visit() cannot see it. diff --git a/Analysis/src/Subtyping.cpp b/Analysis/src/Subtyping.cpp index f45f6d3e..7e7c8cd6 100644 --- a/Analysis/src/Subtyping.cpp +++ b/Analysis/src/Subtyping.cpp @@ -47,12 +47,12 @@ struct VarianceFlipper bool SubtypingReasoning::operator==(const SubtypingReasoning& other) const { - return subPath == other.subPath && superPath == other.superPath; + return subPath == other.subPath && superPath == other.superPath && variance == other.variance; } size_t SubtypingReasoningHash::operator()(const SubtypingReasoning& r) const { - return TypePath::PathHash()(r.subPath) ^ (TypePath::PathHash()(r.superPath) << 1); + return TypePath::PathHash()(r.subPath) ^ (TypePath::PathHash()(r.superPath) << 1) ^ (static_cast(r.variance) << 1); } SubtypingResult& SubtypingResult::andAlso(const SubtypingResult& other) @@ -162,6 +162,19 @@ SubtypingResult& SubtypingResult::withSuperPath(TypePath::Path path) return *this; } +SubtypingResult& SubtypingResult::withVariance(SubtypingVariance variance) +{ + if (reasoning.empty()) + reasoning.insert(SubtypingReasoning{TypePath::kEmpty, TypePath::kEmpty, variance}); + else + { + for (auto& r : reasoning) + r.variance = variance; + } + + return *this; +} + SubtypingResult SubtypingResult::negate(const SubtypingResult& result) { return SubtypingResult{ @@ -671,7 +684,7 @@ SubtypingResult Subtyping::isContravariantWith(SubtypingEnvironment& env, SubTy& template SubtypingResult Subtyping::isInvariantWith(SubtypingEnvironment& env, SubTy&& subTy, SuperTy&& superTy) { - return isCovariantWith(env, subTy, superTy).andAlso(isContravariantWith(env, subTy, superTy)); + return isCovariantWith(env, subTy, superTy).andAlso(isContravariantWith(env, subTy, superTy)).withVariance(SubtypingVariance::Invariant); } template @@ -689,7 +702,7 @@ SubtypingResult Subtyping::isContravariantWith(SubtypingEnvironment& env, const template SubtypingResult Subtyping::isInvariantWith(SubtypingEnvironment& env, const TryPair& pair) { - return isCovariantWith(env, pair).andAlso(isContravariantWith(pair)); + return isCovariantWith(env, pair).andAlso(isContravariantWith(pair)).withVariance(SubtypingVariance::Invariant); } /* @@ -1009,7 +1022,7 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const Meta SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const MetatableType* subMt, const TableType* superTable) { - if (auto subTable = get(subMt->table)) + if (auto subTable = get(follow(subMt->table))) { // Metatables cannot erase properties from the table they're attached to, so // the subtyping rule for this is just if the table component is a subtype diff --git a/Analysis/src/ToDot.cpp b/Analysis/src/ToDot.cpp index 09851024..c4241711 100644 --- a/Analysis/src/ToDot.cpp +++ b/Analysis/src/ToDot.cpp @@ -261,6 +261,14 @@ void StateDot::visitChildren(TypeId ty, int index) visitChild(t.upperBound, index, "[upperBound]"); } } + else if constexpr (std::is_same_v) + { + formatAppend(result, "LocalType"); + finishNodeLabel(ty); + finishNode(); + + visitChild(t.domain, 1, "[domain]"); + } else if constexpr (std::is_same_v) { formatAppend(result, "AnyType %d", index); diff --git a/Analysis/src/ToString.cpp b/Analysis/src/ToString.cpp index cc01d626..4f7869d0 100644 --- a/Analysis/src/ToString.cpp +++ b/Analysis/src/ToString.cpp @@ -100,6 +100,16 @@ struct FindCyclicTypes final : TypeVisitor return false; } + bool visit(TypeId ty, const LocalType& lt) override + { + if (!visited.insert(ty).second) + return false; + + traverse(lt.domain); + + return false; + } + bool visit(TypeId ty, const TableType& ttv) override { if (!visited.insert(ty).second) @@ -500,6 +510,15 @@ struct TypeStringifier } } + void operator()(TypeId ty, const LocalType& lt) + { + state.emit("l-"); + state.emit(lt.name); + state.emit("=["); + stringify(lt.domain); + state.emit("]"); + } + void operator()(TypeId, const BoundType& btv) { stringify(btv.boundTo); diff --git a/Analysis/src/TypeAttach.cpp b/Analysis/src/TypeAttach.cpp index fb47471c..0e246204 100644 --- a/Analysis/src/TypeAttach.cpp +++ b/Analysis/src/TypeAttach.cpp @@ -329,10 +329,14 @@ public: { return Luau::visit(*this, bound.boundTo->ty); } - AstType* operator()(const FreeType& ftv) + AstType* operator()(const FreeType& ft) { return allocator->alloc(Location(), std::nullopt, AstName("free"), std::nullopt, Location()); } + AstType* operator()(const LocalType& lt) + { + return Luau::visit(*this, lt.domain->ty); + } AstType* operator()(const UnionType& uv) { AstArray unionTypes; diff --git a/Analysis/src/TypeChecker2.cpp b/Analysis/src/TypeChecker2.cpp index 8df78140..0250817c 100644 --- a/Analysis/src/TypeChecker2.cpp +++ b/Analysis/src/TypeChecker2.cpp @@ -2464,13 +2464,16 @@ struct TypeChecker2 if (!get2(*subLeaf, *superLeaf) && !get2(*subLeaf, *superLeaf)) ice->ice("Subtyping test returned a reasoning where one path ends at a type and the other ends at a pack.", location); + std::string relation = "a subtype of"; + if (reasoning.variance == SubtypingVariance::Invariant) + relation = "exactly"; + std::string reason; if (reasoning.subPath == reasoning.superPath) - reason = "at " + toString(reasoning.subPath) + ", " + toString(*subLeaf) + " is not a subtype of " + toString(*superLeaf); + reason = "at " + toString(reasoning.subPath) + ", " + toString(*subLeaf) + " is not " + relation + " " + toString(*superLeaf); else - reason = "type " + toString(subTy) + toString(reasoning.subPath, /* prefixDot */ true) + " (" + toString(*subLeaf) + - ") is not a subtype of " + toString(superTy) + toString(reasoning.superPath, /* prefixDot */ true) + " (" + - toString(*superLeaf) + ")"; + reason = "type " + toString(subTy) + toString(reasoning.subPath, /* prefixDot */ true) + " (" + toString(*subLeaf) + ") is not " + + relation + " " + toString(superTy) + toString(reasoning.superPath, /* prefixDot */ true) + " (" + toString(*superLeaf) + ")"; reasons.push_back(reason); } diff --git a/CLI/Bytecode.cpp b/CLI/Bytecode.cpp new file mode 100644 index 00000000..5002ce1d --- /dev/null +++ b/CLI/Bytecode.cpp @@ -0,0 +1,295 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#include "lua.h" +#include "lualib.h" + +#include "Luau/CodeGen.h" +#include "Luau/Compiler.h" +#include "Luau/BytecodeBuilder.h" +#include "Luau/Parser.h" +#include "Luau/BytecodeSummary.h" +#include "FileUtils.h" +#include "Flags.h" + +#include + +using Luau::CodeGen::FunctionBytecodeSummary; + +struct GlobalOptions +{ + int optimizationLevel = 1; + int debugLevel = 1; +} globalOptions; + +static Luau::CompileOptions copts() +{ + Luau::CompileOptions result = {}; + result.optimizationLevel = globalOptions.optimizationLevel; + result.debugLevel = globalOptions.debugLevel; + + return result; +} + +static void displayHelp(const char* argv0) +{ + printf("Usage: %s [options] [file list]\n", argv0); + printf("\n"); + printf("Available options:\n"); + printf(" -h, --help: Display this usage message.\n"); + printf(" -O: compile with optimization level n (default 1, n should be between 0 and 2).\n"); + printf(" -g: compile with debug level n (default 1, n should be between 0 and 2).\n"); + printf(" --fflags=: flags to be enabled.\n"); + printf(" --summary-file=: file in which bytecode analysis summary will be recorded (default 'bytecode-summary.json').\n"); + + exit(0); +} + +static bool parseArgs(int argc, char** argv, std::string& summaryFile) +{ + for (int i = 1; i < argc; i++) + { + if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) + { + displayHelp(argv[0]); + } + else if (strncmp(argv[i], "-O", 2) == 0) + { + int level = atoi(argv[i] + 2); + if (level < 0 || level > 2) + { + fprintf(stderr, "Error: Optimization level must be between 0 and 2 inclusive.\n"); + return false; + } + globalOptions.optimizationLevel = level; + } + else if (strncmp(argv[i], "-g", 2) == 0) + { + int level = atoi(argv[i] + 2); + if (level < 0 || level > 2) + { + fprintf(stderr, "Error: Debug level must be between 0 and 2 inclusive.\n"); + return false; + } + globalOptions.debugLevel = level; + } + else if (strncmp(argv[i], "--summary-file=", 15) == 0) + { + summaryFile = argv[i] + 15; + + if (summaryFile.size() == 0) + { + fprintf(stderr, "Error: filename missing for '--summary-file'.\n\n"); + return false; + } + } + else if (strncmp(argv[i], "--fflags=", 9) == 0) + { + setLuauFlags(argv[i] + 9); + } + else if (argv[i][0] == '-') + { + fprintf(stderr, "Error: Unrecognized option '%s'.\n\n", argv[i]); + displayHelp(argv[0]); + } + } + + return true; +} + +static void report(const char* name, const Luau::Location& location, const char* type, const char* message) +{ + fprintf(stderr, "%s(%d,%d): %s: %s\n", name, location.begin.line + 1, location.begin.column + 1, type, message); +} + +static void reportError(const char* name, const Luau::ParseError& error) +{ + report(name, error.getLocation(), "SyntaxError", error.what()); +} + +static void reportError(const char* name, const Luau::CompileError& error) +{ + report(name, error.getLocation(), "CompileError", error.what()); +} + +static bool analyzeFile(const char* name, const unsigned nestingLimit, std::vector& summaries) +{ + std::optional source = readFile(name); + + if (!source) + { + fprintf(stderr, "Error opening %s\n", name); + return false; + } + + try + { + Luau::BytecodeBuilder bcb; + + compileOrThrow(bcb, source.value(), copts()); + + const std::string& bytecode = bcb.getBytecode(); + + std::unique_ptr globalState(luaL_newstate(), lua_close); + lua_State* L = globalState.get(); + + if (luau_load(L, name, bytecode.data(), bytecode.size(), 0) == 0) + { + summaries = Luau::CodeGen::summarizeBytecode(L, -1, nestingLimit); + return true; + } + else + { + fprintf(stderr, "Error loading bytecode %s\n", name); + return false; + } + } + catch (Luau::ParseErrors& e) + { + for (auto& error : e.getErrors()) + reportError(name, error); + return false; + } + catch (Luau::CompileError& e) + { + reportError(name, e); + return false; + } + + return true; +} + +static std::string escapeFilename(const std::string& filename) +{ + std::string escaped; + escaped.reserve(filename.size()); + + for (const char ch : filename) + { + switch (ch) + { + case '\\': + escaped.push_back('/'); + break; + case '"': + escaped.push_back('\\'); + escaped.push_back(ch); + break; + default: + escaped.push_back(ch); + } + } + + return escaped; +} + +static void serializeFunctionSummary(const FunctionBytecodeSummary& summary, FILE* fp) +{ + const unsigned nestingLimit = summary.getNestingLimit(); + const unsigned opLimit = summary.getOpLimit(); + + fprintf(fp, " {\n"); + fprintf(fp, " \"source\": \"%s\",\n", summary.getSource().c_str()); + fprintf(fp, " \"name\": \"%s\",\n", summary.getName().c_str()); + fprintf(fp, " \"line\": %d,\n", summary.getLine()); + fprintf(fp, " \"nestingLimit\": %u,\n", nestingLimit); + fprintf(fp, " \"counts\": ["); + + for (unsigned nesting = 0; nesting <= nestingLimit; ++nesting) + { + fprintf(fp, "\n ["); + + for (unsigned i = 0; i < opLimit; ++i) + { + fprintf(fp, "%d", summary.getCount(nesting, uint8_t(i))); + if (i < opLimit - 1) + fprintf(fp, ", "); + } + + fprintf(fp, "]"); + if (nesting < nestingLimit) + fprintf(fp, ","); + } + + fprintf(fp, "\n ]"); + fprintf(fp, "\n }"); +} + +static void serializeScriptSummary(const std::string& file, const std::vector& scriptSummary, FILE* fp) +{ + std::string escaped(escapeFilename(file)); + const size_t functionCount = scriptSummary.size(); + + fprintf(fp, " \"%s\": [\n", escaped.c_str()); + + for (size_t i = 0; i < functionCount; ++i) + { + serializeFunctionSummary(scriptSummary[i], fp); + fprintf(fp, i == (functionCount - 1) ? "\n" : ",\n"); + } + + fprintf(fp, " ]"); +} + +static bool serializeSummaries( + const std::vector& files, const std::vector>& scriptSummaries, const std::string& summaryFile) +{ + + FILE* fp = fopen(summaryFile.c_str(), "w"); + const size_t fileCount = files.size(); + + if (!fp) + { + fprintf(stderr, "Unable to open '%s'.\n", summaryFile.c_str()); + return false; + } + + fprintf(fp, "{\n"); + + for (size_t i = 0; i < fileCount; ++i) + { + serializeScriptSummary(files[i], scriptSummaries[i], fp); + fprintf(fp, i < (fileCount - 1) ? ",\n" : "\n"); + } + + fprintf(fp, "}"); + fclose(fp); + + return true; +} + +static int assertionHandler(const char* expr, const char* file, int line, const char* function) +{ + printf("%s(%d): ASSERTION FAILED: %s\n", file, line, expr); + return 1; +} + +int main(int argc, char** argv) +{ + Luau::assertHandler() = assertionHandler; + + setLuauFlagsDefault(); + + std::string summaryFile("bytecode-summary.json"); + unsigned nestingLimit = 0; + + if (!parseArgs(argc, argv, summaryFile)) + return 1; + + const std::vector files = getSourceFiles(argc, argv); + size_t fileCount = files.size(); + + std::vector> scriptSummaries; + scriptSummaries.reserve(fileCount); + + for (size_t i = 0; i < fileCount; ++i) + { + if (!analyzeFile(files[i].c_str(), nestingLimit, scriptSummaries[i])) + return 1; + } + + if (!serializeSummaries(files, scriptSummaries, summaryFile)) + return 1; + + fprintf(stdout, "Bytecode summary written to '%s'\n", summaryFile.c_str()); + + return 0; +} diff --git a/CMakeLists.txt b/CMakeLists.txt index 0dbfbee1..9f906fb2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -37,6 +37,7 @@ if(LUAU_BUILD_CLI) add_executable(Luau.Ast.CLI) add_executable(Luau.Reduce.CLI) add_executable(Luau.Compile.CLI) + add_executable(Luau.Bytecode.CLI) # This also adds target `name` on Linux/macOS and `name.exe` on Windows set_target_properties(Luau.Repl.CLI PROPERTIES OUTPUT_NAME luau) @@ -44,6 +45,7 @@ if(LUAU_BUILD_CLI) set_target_properties(Luau.Ast.CLI PROPERTIES OUTPUT_NAME luau-ast) set_target_properties(Luau.Reduce.CLI PROPERTIES OUTPUT_NAME luau-reduce) set_target_properties(Luau.Compile.CLI PROPERTIES OUTPUT_NAME luau-compile) + set_target_properties(Luau.Bytecode.CLI PROPERTIES OUTPUT_NAME luau-bytecode) endif() if(LUAU_BUILD_TESTS) @@ -187,6 +189,7 @@ if(LUAU_BUILD_CLI) target_compile_options(Luau.Analyze.CLI PRIVATE ${LUAU_OPTIONS}) target_compile_options(Luau.Ast.CLI PRIVATE ${LUAU_OPTIONS}) target_compile_options(Luau.Compile.CLI PRIVATE ${LUAU_OPTIONS}) + target_compile_options(Luau.Bytecode.CLI PRIVATE ${LUAU_OPTIONS}) target_include_directories(Luau.Repl.CLI PRIVATE extern extern/isocline/include) @@ -209,6 +212,8 @@ if(LUAU_BUILD_CLI) target_link_libraries(Luau.Reduce.CLI PRIVATE Luau.Common Luau.Ast Luau.Analysis) target_link_libraries(Luau.Compile.CLI PRIVATE Luau.Compiler Luau.VM Luau.CodeGen) + + target_link_libraries(Luau.Bytecode.CLI PRIVATE Luau.Compiler Luau.VM Luau.CodeGen) endif() if(LUAU_BUILD_TESTS) diff --git a/CodeGen/include/Luau/BytecodeSummary.h b/CodeGen/include/Luau/BytecodeSummary.h new file mode 100644 index 00000000..cfdd5f84 --- /dev/null +++ b/CodeGen/include/Luau/BytecodeSummary.h @@ -0,0 +1,81 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#pragma once + +#include "Luau/Common.h" +#include "Luau/Bytecode.h" + +#include +#include + +struct lua_State; +struct Proto; + +namespace Luau +{ +namespace CodeGen +{ + +class FunctionBytecodeSummary +{ +public: + FunctionBytecodeSummary(std::string source, std::string name, const int line, unsigned nestingLimit); + + const std::string& getSource() const + { + return source; + } + + const std::string& getName() const + { + return name; + } + + int getLine() const + { + return line; + } + + const unsigned getNestingLimit() const + { + return nestingLimit; + } + + const unsigned getOpLimit() const + { + return LOP__COUNT; + } + + void incCount(unsigned nesting, uint8_t op) + { + LUAU_ASSERT(nesting <= getNestingLimit()); + LUAU_ASSERT(op < getOpLimit()); + ++counts[nesting][op]; + } + + unsigned getCount(unsigned nesting, uint8_t op) const + { + LUAU_ASSERT(nesting <= getNestingLimit()); + LUAU_ASSERT(op < getOpLimit()); + return counts[nesting][op]; + } + + const std::vector& getCounts(unsigned nesting) const + { + LUAU_ASSERT(nesting <= getNestingLimit()); + return counts[nesting]; + } + + static FunctionBytecodeSummary fromProto(Proto* proto, unsigned nestingLimit); + +private: + std::string source; + std::string name; + int line; + unsigned nestingLimit; + std::vector> counts; +}; + +std::vector summarizeBytecode(lua_State* L, int idx, unsigned nestingLimit); + +} // namespace CodeGen +} // namespace Luau diff --git a/CodeGen/src/AssemblyBuilderX64.cpp b/CodeGen/src/AssemblyBuilderX64.cpp index 22978dd4..ec53916f 100644 --- a/CodeGen/src/AssemblyBuilderX64.cpp +++ b/CodeGen/src/AssemblyBuilderX64.cpp @@ -6,6 +6,8 @@ #include #include +LUAU_FASTFLAG(LuauCodeGenFixByteLower) + namespace Luau { namespace CodeGen @@ -1437,10 +1439,18 @@ void AssemblyBuilderX64::placeImm8(int32_t imm) { int8_t imm8 = int8_t(imm); - if (imm8 == imm) + if (FFlag::LuauCodeGenFixByteLower) + { + LUAU_ASSERT(imm8 == imm); place(imm8); + } else - LUAU_ASSERT(!"Invalid immediate value"); + { + if (imm8 == imm) + place(imm8); + else + LUAU_ASSERT(!"Invalid immediate value"); + } } void AssemblyBuilderX64::placeImm16(int16_t imm) diff --git a/CodeGen/src/BytecodeSummary.cpp b/CodeGen/src/BytecodeSummary.cpp new file mode 100644 index 00000000..7bf38cc4 --- /dev/null +++ b/CodeGen/src/BytecodeSummary.cpp @@ -0,0 +1,71 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#include "Luau/BytecodeSummary.h" +#include "CodeGenLower.h" + +#include "lua.h" +#include "lapi.h" +#include "lobject.h" +#include "lstate.h" + +namespace Luau +{ +namespace CodeGen +{ + +FunctionBytecodeSummary::FunctionBytecodeSummary(std::string source, std::string name, const int line, unsigned nestingLimit) + : source(std::move(source)) + , name(std::move(name)) + , line(line) + , nestingLimit(nestingLimit) +{ + counts.reserve(nestingLimit); + for (unsigned i = 0; i < 1 + nestingLimit; ++i) + { + counts.push_back(std::vector(getOpLimit(), 0)); + } +} + +FunctionBytecodeSummary FunctionBytecodeSummary::fromProto(Proto* proto, unsigned nestingLimit) +{ + const char* source = getstr(proto->source); + source = (source[0] == '=' || source[0] == '@') ? source + 1 : "[string]"; + + const char* name = proto->debugname ? getstr(proto->debugname) : ""; + + int line = proto->linedefined; + + FunctionBytecodeSummary summary(source, name, line, nestingLimit); + + for (int i = 0; i < proto->sizecode; ++i) + { + Instruction insn = proto->code[i]; + uint8_t op = LUAU_INSN_OP(insn); + summary.incCount(0, op); + } + + return summary; +} + +std::vector summarizeBytecode(lua_State* L, int idx, unsigned nestingLimit) +{ + LUAU_ASSERT(lua_isLfunction(L, idx)); + const TValue* func = luaA_toobject(L, idx); + + Proto* root = clvalue(func)->l.p; + + std::vector protos; + gatherFunctions(protos, root, CodeGen_ColdFunctions); + + std::vector summaries; + summaries.reserve(protos.size()); + + for (Proto* proto : protos) + { + summaries.push_back(FunctionBytecodeSummary::fromProto(proto, nestingLimit)); + } + + return summaries; +} + +} // namespace CodeGen +} // namespace Luau diff --git a/CodeGen/src/IrLoweringA64.cpp b/CodeGen/src/IrLoweringA64.cpp index 42450d3c..6a1733a5 100644 --- a/CodeGen/src/IrLoweringA64.cpp +++ b/CodeGen/src/IrLoweringA64.cpp @@ -405,7 +405,15 @@ void IrLoweringA64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next) case IrCmd::STORE_POINTER: { AddressA64 addr = tempAddr(inst.a, offsetof(TValue, value)); - build.str(regOp(inst.b), addr); + if (inst.b.kind == IrOpKind::Constant) + { + LUAU_ASSERT(intOp(inst.b) == 0); + build.str(xzr, addr); + } + else + { + build.str(regOp(inst.b), addr); + } break; } case IrCmd::STORE_DOUBLE: diff --git a/CodeGen/src/IrLoweringX64.cpp b/CodeGen/src/IrLoweringX64.cpp index f7572a6c..74a5bfd6 100644 --- a/CodeGen/src/IrLoweringX64.cpp +++ b/CodeGen/src/IrLoweringX64.cpp @@ -15,6 +15,8 @@ #include "lstate.h" #include "lgc.h" +LUAU_FASTFLAG(LuauCodeGenFixByteLower) + namespace Luau { namespace CodeGen @@ -213,11 +215,24 @@ void IrLoweringX64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next) LUAU_ASSERT(!"Unsupported instruction form"); break; case IrCmd::STORE_POINTER: - if (inst.a.kind == IrOpKind::Inst) - build.mov(qword[regOp(inst.a) + offsetof(TValue, value)], regOp(inst.b)); + { + OperandX64 valueLhs = inst.a.kind == IrOpKind::Inst ? qword[regOp(inst.a) + offsetof(TValue, value)] : luauRegValue(vmRegOp(inst.a)); + + if (inst.b.kind == IrOpKind::Constant) + { + LUAU_ASSERT(intOp(inst.b) == 0); + build.mov(valueLhs, 0); + } + else if (inst.b.kind == IrOpKind::Inst) + { + build.mov(valueLhs, regOp(inst.b)); + } else - build.mov(luauRegValue(vmRegOp(inst.a)), regOp(inst.b)); + { + LUAU_ASSERT(!"Unsupported instruction form"); + } break; + } case IrCmd::STORE_DOUBLE: { OperandX64 valueLhs = inst.a.kind == IrOpKind::Inst ? qword[regOp(inst.a) + offsetof(TValue, value)] : luauRegValue(vmRegOp(inst.a)); @@ -1787,9 +1802,18 @@ void IrLoweringX64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next) case IrCmd::BUFFER_WRITEI8: { - OperandX64 value = inst.c.kind == IrOpKind::Inst ? byteReg(regOp(inst.c)) : OperandX64(intOp(inst.c)); + if (FFlag::LuauCodeGenFixByteLower) + { + OperandX64 value = inst.c.kind == IrOpKind::Inst ? byteReg(regOp(inst.c)) : OperandX64(int8_t(intOp(inst.c))); - build.mov(byte[bufferAddrOp(inst.a, inst.b)], value); + build.mov(byte[bufferAddrOp(inst.a, inst.b)], value); + } + else + { + 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; } @@ -1807,9 +1831,18 @@ void IrLoweringX64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next) case IrCmd::BUFFER_WRITEI16: { - OperandX64 value = inst.c.kind == IrOpKind::Inst ? wordReg(regOp(inst.c)) : OperandX64(intOp(inst.c)); + if (FFlag::LuauCodeGenFixByteLower) + { + OperandX64 value = inst.c.kind == IrOpKind::Inst ? wordReg(regOp(inst.c)) : OperandX64(int16_t(intOp(inst.c))); - build.mov(word[bufferAddrOp(inst.a, inst.b)], value); + build.mov(word[bufferAddrOp(inst.a, inst.b)], value); + } + else + { + 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; } diff --git a/CodeGen/src/IrTranslation.cpp b/CodeGen/src/IrTranslation.cpp index dff7002d..91e87fdb 100644 --- a/CodeGen/src/IrTranslation.cpp +++ b/CodeGen/src/IrTranslation.cpp @@ -14,6 +14,7 @@ LUAU_FASTFLAGVARIABLE(LuauLowerAltLoopForn, false) LUAU_FASTFLAG(LuauImproveInsertIr) +LUAU_FASTFLAGVARIABLE(LuauFullLoopLuserdata, false) namespace Luau { @@ -808,7 +809,7 @@ void translateInstForGPrepNext(IrBuilder& build, const Instruction* pc, int pcpo build.inst(IrCmd::STORE_TAG, build.vmReg(ra), build.constTag(LUA_TNIL)); // setpvalue(ra + 2, reinterpret_cast(uintptr_t(0))); - build.inst(IrCmd::STORE_INT, build.vmReg(ra + 2), build.constInt(0)); + build.inst(FFlag::LuauFullLoopLuserdata ? IrCmd::STORE_POINTER : IrCmd::STORE_INT, build.vmReg(ra + 2), build.constInt(0)); build.inst(IrCmd::STORE_TAG, build.vmReg(ra + 2), build.constTag(LUA_TLIGHTUSERDATA)); build.inst(IrCmd::JUMP, target); @@ -840,7 +841,7 @@ void translateInstForGPrepInext(IrBuilder& build, const Instruction* pc, int pcp build.inst(IrCmd::STORE_TAG, build.vmReg(ra), build.constTag(LUA_TNIL)); // setpvalue(ra + 2, reinterpret_cast(uintptr_t(0))); - build.inst(IrCmd::STORE_INT, build.vmReg(ra + 2), build.constInt(0)); + build.inst(FFlag::LuauFullLoopLuserdata ? IrCmd::STORE_POINTER : IrCmd::STORE_INT, build.vmReg(ra + 2), build.constInt(0)); build.inst(IrCmd::STORE_TAG, build.vmReg(ra + 2), build.constTag(LUA_TLIGHTUSERDATA)); build.inst(IrCmd::JUMP, target); diff --git a/CodeGen/src/OptimizeConstProp.cpp b/CodeGen/src/OptimizeConstProp.cpp index 8d0f829a..45583fcc 100644 --- a/CodeGen/src/OptimizeConstProp.cpp +++ b/CodeGen/src/OptimizeConstProp.cpp @@ -17,6 +17,7 @@ LUAU_FASTINTVARIABLE(LuauCodeGenReuseSlotLimit, 64) LUAU_FASTFLAGVARIABLE(DebugLuauAbortingChecks, false) LUAU_FASTFLAGVARIABLE(LuauReuseArrSlots2, false) LUAU_FASTFLAG(LuauLowerAltLoopForn) +LUAU_FASTFLAGVARIABLE(LuauCodeGenFixByteLower, false) namespace Luau { @@ -618,15 +619,19 @@ static void constPropInInst(ConstPropState& state, IrBuilder& build, IrFunction& if (inst.a.kind == IrOpKind::VmReg) { state.invalidateValue(inst.a); - state.forwardVmRegStoreToLoad(inst, IrCmd::LOAD_POINTER); - if (IrInst* instOp = function.asInstOp(inst.b); instOp && instOp->cmd == IrCmd::NEW_TABLE) + if (inst.b.kind == IrOpKind::Inst) { - if (RegisterInfo* info = state.tryGetRegisterInfo(inst.a)) + state.forwardVmRegStoreToLoad(inst, IrCmd::LOAD_POINTER); + + if (IrInst* instOp = function.asInstOp(inst.b); instOp && instOp->cmd == IrCmd::NEW_TABLE) { - info->knownNotReadonly = true; - info->knownNoMetatable = true; - info->knownTableArraySize = function.uintOp(instOp->a); + if (RegisterInfo* info = state.tryGetRegisterInfo(inst.a)) + { + info->knownNotReadonly = true; + info->knownNoMetatable = true; + info->knownTableArraySize = function.uintOp(instOp->a); + } } } } diff --git a/Common/include/Luau/DenseHash.h b/Common/include/Luau/DenseHash.h index 067a9d7a..f175b169 100644 --- a/Common/include/Luau/DenseHash.h +++ b/Common/include/Luau/DenseHash.h @@ -540,7 +540,7 @@ public: return impl.end(); } - bool operator==(const DenseHashSet& other) + bool operator==(const DenseHashSet& other) const { if (size() != other.size()) return false; @@ -554,7 +554,7 @@ public: return true; } - bool operator!=(const DenseHashSet& other) + bool operator!=(const DenseHashSet& other) const { return !(*this == other); } diff --git a/Makefile b/Makefile index 9e97633f..4d606bef 100644 --- a/Makefile +++ b/Makefile @@ -54,6 +54,10 @@ COMPILE_CLI_SOURCES=CLI/FileUtils.cpp CLI/Flags.cpp CLI/Compile.cpp COMPILE_CLI_OBJECTS=$(COMPILE_CLI_SOURCES:%=$(BUILD)/%.o) COMPILE_CLI_TARGET=$(BUILD)/luau-compile +BYTECODE_CLI_SOURCES=CLI/FileUtils.cpp CLI/Flags.cpp CLI/Bytecode.cpp +BYTECODE_CLI_OBJECTS=$(BYTECODE_CLI_SOURCES:%=$(BUILD)/%.o) +BYTECODE_CLI_TARGET=$(BUILD)/luau-bytecode + FUZZ_SOURCES=$(wildcard fuzz/*.cpp) fuzz/luau.pb.cpp FUZZ_OBJECTS=$(FUZZ_SOURCES:%=$(BUILD)/%.o) @@ -65,8 +69,8 @@ ifneq ($(opt),) TESTS_ARGS+=-O$(opt) endif -OBJECTS=$(AST_OBJECTS) $(COMPILER_OBJECTS) $(CONFIG_OBJECTS) $(ANALYSIS_OBJECTS) $(CODEGEN_OBJECTS) $(VM_OBJECTS) $(ISOCLINE_OBJECTS) $(TESTS_OBJECTS) $(REPL_CLI_OBJECTS) $(ANALYZE_CLI_OBJECTS) $(COMPILE_CLI_OBJECTS) $(FUZZ_OBJECTS) -EXECUTABLE_ALIASES = luau luau-analyze luau-compile luau-tests +OBJECTS=$(AST_OBJECTS) $(COMPILER_OBJECTS) $(CONFIG_OBJECTS) $(ANALYSIS_OBJECTS) $(CODEGEN_OBJECTS) $(VM_OBJECTS) $(ISOCLINE_OBJECTS) $(TESTS_OBJECTS) $(REPL_CLI_OBJECTS) $(ANALYZE_CLI_OBJECTS) $(COMPILE_CLI_OBJECTS) $(BYTECODE_CLI_OBJECTS) $(FUZZ_OBJECTS) +EXECUTABLE_ALIASES = luau luau-analyze luau-compile luau-bytecode luau-tests # common flags CXXFLAGS=-g -Wall @@ -142,6 +146,7 @@ $(TESTS_OBJECTS): CXXFLAGS+=-std=c++17 -ICommon/include -IAst/include -ICompiler $(REPL_CLI_OBJECTS): CXXFLAGS+=-std=c++17 -ICommon/include -IAst/include -ICompiler/include -IVM/include -ICodeGen/include -Iextern -Iextern/isocline/include $(ANALYZE_CLI_OBJECTS): CXXFLAGS+=-std=c++17 -ICommon/include -IAst/include -IAnalysis/include -IConfig/include -Iextern $(COMPILE_CLI_OBJECTS): CXXFLAGS+=-std=c++17 -ICommon/include -IAst/include -ICompiler/include -IVM/include -ICodeGen/include +$(BYTECODE_CLI_OBJECTS): CXXFLAGS+=-std=c++17 -ICommon/include -IAst/include -ICompiler/include -IVM/include -ICodeGen/include $(FUZZ_OBJECTS): CXXFLAGS+=-std=c++17 -ICommon/include -IAst/include -ICompiler/include -IAnalysis/include -IVM/include -ICodeGen/include -IConfig/include $(TESTS_TARGET): LDFLAGS+=-lpthread @@ -206,6 +211,9 @@ luau-analyze: $(ANALYZE_CLI_TARGET) luau-compile: $(COMPILE_CLI_TARGET) ln -fs $^ $@ +luau-bytecode: $(BYTECODE_CLI_TARGET) + ln -fs $^ $@ + luau-tests: $(TESTS_TARGET) ln -fs $^ $@ @@ -214,8 +222,9 @@ $(TESTS_TARGET): $(TESTS_OBJECTS) $(ANALYSIS_TARGET) $(COMPILER_TARGET) $(CONFIG $(REPL_CLI_TARGET): $(REPL_CLI_OBJECTS) $(COMPILER_TARGET) $(AST_TARGET) $(CODEGEN_TARGET) $(VM_TARGET) $(ISOCLINE_TARGET) $(ANALYZE_CLI_TARGET): $(ANALYZE_CLI_OBJECTS) $(ANALYSIS_TARGET) $(AST_TARGET) $(CONFIG_TARGET) $(COMPILE_CLI_TARGET): $(COMPILE_CLI_OBJECTS) $(COMPILER_TARGET) $(AST_TARGET) $(CODEGEN_TARGET) $(VM_TARGET) +$(BYTECODE_CLI_TARGET): $(BYTECODE_CLI_OBJECTS) $(COMPILER_TARGET) $(AST_TARGET) $(CODEGEN_TARGET) $(VM_TARGET) -$(TESTS_TARGET) $(REPL_CLI_TARGET) $(ANALYZE_CLI_TARGET) $(COMPILE_CLI_TARGET): +$(TESTS_TARGET) $(REPL_CLI_TARGET) $(ANALYZE_CLI_TARGET) $(COMPILE_CLI_TARGET) $(BYTECODE_CLI_TARGET): $(CXX) $^ $(LDFLAGS) -o $@ # executable targets for fuzzing diff --git a/Sources.cmake b/Sources.cmake index 929c99a4..eee66b86 100644 --- a/Sources.cmake +++ b/Sources.cmake @@ -92,6 +92,7 @@ target_sources(Luau.CodeGen PRIVATE CodeGen/include/Luau/UnwindBuilder.h CodeGen/include/Luau/UnwindBuilderDwarf2.h CodeGen/include/Luau/UnwindBuilderWin.h + CodeGen/include/Luau/BytecodeSummary.h CodeGen/include/luacodegen.h CodeGen/src/AssemblyBuilderA64.cpp @@ -124,6 +125,7 @@ target_sources(Luau.CodeGen PRIVATE CodeGen/src/OptimizeFinalX64.cpp CodeGen/src/UnwindBuilderDwarf2.cpp CodeGen/src/UnwindBuilderWin.cpp + CodeGen/src/BytecodeSummary.cpp CodeGen/src/BitUtils.h CodeGen/src/ByteUtils.h @@ -518,3 +520,13 @@ if(TARGET Luau.Compile.CLI) CLI/Flags.cpp CLI/Compile.cpp) endif() + +if(TARGET Luau.Bytecode.CLI) + # Luau.Bytecode.CLI Sources + target_sources(Luau.Bytecode.CLI PRIVATE + CLI/FileUtils.h + CLI/FileUtils.cpp + CLI/Flags.h + CLI/Flags.cpp + CLI/Bytecode.cpp) +endif() diff --git a/tests/Conformance.test.cpp b/tests/Conformance.test.cpp index 968a55be..db4941ee 100644 --- a/tests/Conformance.test.cpp +++ b/tests/Conformance.test.cpp @@ -10,7 +10,9 @@ #include "Luau/TypeInfer.h" #include "Luau/BytecodeBuilder.h" #include "Luau/Frontend.h" +#include "Luau/Compiler.h" #include "Luau/CodeGen.h" +#include "Luau/BytecodeSummary.h" #include "doctest.h" #include "ScopedFlags.h" @@ -271,6 +273,25 @@ static void* limitedRealloc(void* ud, void* ptr, size_t osize, size_t nsize) } } +static std::vector analyzeFile(const char* source, const unsigned nestingLimit) +{ + Luau::BytecodeBuilder bcb; + + Luau::CompileOptions options; + options.optimizationLevel = optimizationLevel; + options.debugLevel = 1; + + compileOrThrow(bcb, source, options); + + const std::string& bytecode = bcb.getBytecode(); + + std::unique_ptr globalState(luaL_newstate(), lua_close); + lua_State* L = globalState.get(); + + LUAU_ASSERT(luau_load(L, "source", bytecode.data(), bytecode.size(), 0) == 0); + return Luau::CodeGen::summarizeBytecode(L, -1, nestingLimit); +} + TEST_SUITE_BEGIN("Conformance"); TEST_CASE("CodegenSupported") @@ -292,6 +313,7 @@ TEST_CASE("Basic") TEST_CASE("Buffers") { ScopedFastFlag luauBufferBetterMsg{"LuauBufferBetterMsg", true}; + ScopedFastFlag luauCodeGenFixByteLower{"LuauCodeGenFixByteLower", true}; runConformance("buffers.lua"); } @@ -1988,4 +2010,51 @@ TEST_CASE("HugeFunction") CHECK(lua_tonumber(L, -1) == 42); } +TEST_CASE("BytecodeDistributionPerFunctionTest") +{ + const char* source = R"( +local function first(n, p) + local t = {} + for i=1,p do t[i] = i*10 end + + local function inner(_,n) + if n > 0 then + n = n-1 + return n, unpack(t) + end + end + return inner, nil, n +end + +local function second(x) + return x[1] +end +)"; + + std::vector summaries(analyzeFile(source, 0)); + + CHECK_EQ(summaries[0].getName(), "inner"); + CHECK_EQ(summaries[0].getLine(), 6); + CHECK_EQ(summaries[0].getCounts(0), + std::vector({1, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0})); + + CHECK_EQ(summaries[1].getName(), "first"); + CHECK_EQ(summaries[1].getLine(), 2); + CHECK_EQ(summaries[1].getCounts(0), + std::vector({1, 0, 1, 0, 2, 0, 3, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0})); + + CHECK_EQ(summaries[2].getName(), "second"); + CHECK_EQ(summaries[2].getLine(), 15); + CHECK_EQ(summaries[2].getCounts(0), + std::vector({0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0})); + + CHECK_EQ(summaries[3].getName(), ""); + CHECK_EQ(summaries[3].getLine(), 1); + CHECK_EQ(summaries[3].getCounts(0), + std::vector({0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0})); +} + TEST_SUITE_END(); diff --git a/tests/DataFlowGraph.test.cpp b/tests/DataFlowGraph.test.cpp index e957316e..be0337a2 100644 --- a/tests/DataFlowGraph.test.cpp +++ b/tests/DataFlowGraph.test.cpp @@ -317,4 +317,97 @@ TEST_CASE_FIXTURE(DataFlowGraphFixture, "mutate_property_of_table_owned_by_while CHECK(x1 != x2); } +TEST_CASE_FIXTURE(DataFlowGraphFixture, "property_lookup_on_a_phi_node") +{ + dfg(R"( + local t = {} + t.x = 5 + + if cond() then + t.x = 7 + end + + print(t.x) + )"); + + DefId x1 = getDef(); // t.x = 5 + DefId x2 = getDef(); // t.x = 7 + DefId x3 = getDef(); // print(t.x) + + CHECK(x1 != x2); + CHECK(x2 != x3); + + const Phi* phi = get(x3); + REQUIRE(phi); + CHECK(phi->operands.at(0) == x1); + CHECK(phi->operands.at(1) == x2); +} + +TEST_CASE_FIXTURE(DataFlowGraphFixture, "property_lookup_on_a_phi_node_2") +{ + dfg(R"( + local t = {} + + if cond() then + t.x = 5 + else + t.x = 7 + end + + print(t.x) + )"); + + DefId x1 = getDef(); // t.x = 5 + DefId x2 = getDef(); // t.x = 7 + DefId x3 = getDef(); // print(t.x) + + CHECK(x1 != x2); + CHECK(x2 != x3); + + const Phi* phi = get(x3); + REQUIRE(phi); + CHECK(phi->operands.at(0) == x2); + CHECK(phi->operands.at(1) == x1); +} + +TEST_CASE_FIXTURE(DataFlowGraphFixture, "property_lookup_on_a_phi_node_3") +{ + dfg(R"( + local t = {} + t.x = 3 + + if cond() then + t.x = 5 + t.y = 7 + else + t.z = 42 + end + + print(t.x) + print(t.y) + print(t.z) + )"); + + DefId x1 = getDef(); // t.x = 3 + DefId x2 = getDef(); // t.x = 5 + + DefId y1 = getDef(); // t.y = 7 + + DefId z1 = getDef(); // t.z = 42 + + DefId x3 = getDef(); // print(t.x) + DefId y2 = getDef(); // print(t.y) + DefId z2 = getDef(); // print(t.z) + + CHECK(x1 != x2); + CHECK(x2 != x3); + CHECK(y1 == y2); + CHECK(z1 == z2); + + const Phi* phi = get(x3); + REQUIRE(phi); + CHECK(phi->operands.at(0) == x1); + CHECK(phi->operands.at(1) == x2); +} + TEST_SUITE_END(); diff --git a/tests/Fixture.cpp b/tests/Fixture.cpp index 8f40f574..b3a46e49 100644 --- a/tests/Fixture.cpp +++ b/tests/Fixture.cpp @@ -412,7 +412,7 @@ TypeId Fixture::requireTypeAlias(const std::string& name) { std::optional ty = lookupType(name); REQUIRE(ty); - return *ty; + return follow(*ty); } TypeId Fixture::requireExportedType(const ModuleName& moduleName, const std::string& name) diff --git a/tests/NonStrictTypeChecker.test.cpp b/tests/NonStrictTypeChecker.test.cpp index 8eb778c8..e65635bd 100644 --- a/tests/NonStrictTypeChecker.test.cpp +++ b/tests/NonStrictTypeChecker.test.cpp @@ -6,12 +6,22 @@ #include "Luau/Common.h" #include "Luau/Ast.h" #include "Luau/ModuleResolver.h" +#include "Luau/VisitType.h" #include "ScopedFlags.h" #include "doctest.h" #include using namespace Luau; +#define NONSTRICT_REQUIRE_CHECKED_ERR(index, name, result) \ + do \ + { \ + REQUIRE(index < result.errors.size()); \ + auto err##index = get(result.errors[index]); \ + REQUIRE(err##index != nullptr); \ + CHECK_EQ((err##index)->checkedFunctionName, name); \ + } while (false) + struct NonStrictTypeCheckerFixture : Fixture { @@ -28,22 +38,167 @@ struct NonStrictTypeCheckerFixture : Fixture std::string definitions = R"BUILTIN_SRC( declare function @checked abs(n: number): number +declare function @checked lower(s: string): string +declare function cond() : boolean )BUILTIN_SRC"; }; TEST_SUITE_BEGIN("NonStrictTypeCheckerTest"); -TEST_CASE_FIXTURE(NonStrictTypeCheckerFixture, "simple_non_strict") +TEST_CASE_FIXTURE(NonStrictTypeCheckerFixture, "simple_non_strict_failure") { - auto res = checkNonStrict(R"BUILTIN_SRC( + CheckResult result = checkNonStrict(R"BUILTIN_SRC( abs("hi") )BUILTIN_SRC"); - LUAU_REQUIRE_ERRORS(res); - REQUIRE(res.errors.size() == 1); - auto err = get(res.errors[0]); - REQUIRE(err != nullptr); - REQUIRE(err->checkedFunctionName == "abs"); + LUAU_REQUIRE_ERROR_COUNT(1, result); + NONSTRICT_REQUIRE_CHECKED_ERR(0, "abs", result); } +TEST_CASE_FIXTURE(NonStrictTypeCheckerFixture, "nested_function_calls_constant") +{ + CheckResult result = checkNonStrict(R"( +local x +abs(lower(x)) +)"); + + LUAU_REQUIRE_ERROR_COUNT(1, result); + NONSTRICT_REQUIRE_CHECKED_ERR(0, "abs", result); +} + + +TEST_CASE_FIXTURE(NonStrictTypeCheckerFixture, "if_then_else_warns_with_never_local") +{ + CheckResult result = checkNonStrict(R"( +local x : never +if cond() then + abs(x) +else + lower(x) +end +)"); + + LUAU_REQUIRE_ERROR_COUNT(2, result); + NONSTRICT_REQUIRE_CHECKED_ERR(0, "abs", result); + NONSTRICT_REQUIRE_CHECKED_ERR(1, "lower", result); +} + +TEST_CASE_FIXTURE(NonStrictTypeCheckerFixture, "if_then_else_warns_nil_branches") +{ + auto result = checkNonStrict(R"( +local x +if cond() then + abs(x) +else + lower(x) +end +)"); + + LUAU_REQUIRE_ERROR_COUNT(2, result); + NONSTRICT_REQUIRE_CHECKED_ERR(0, "abs", result); + NONSTRICT_REQUIRE_CHECKED_ERR(1, "lower", result); +} + +TEST_CASE_FIXTURE(NonStrictTypeCheckerFixture, "if_then_else_doesnt_warn_else_branch") +{ + auto result = checkNonStrict(R"( +local x : string = "hi" +if cond() then + abs(x) +else + lower(x) +end +)"); + + LUAU_REQUIRE_ERROR_COUNT(1, result); + NONSTRICT_REQUIRE_CHECKED_ERR(0, "abs", result); +} + +TEST_CASE_FIXTURE(NonStrictTypeCheckerFixture, "if_then_no_else") +{ + CheckResult result = checkNonStrict(R"( +local x : string +if cond() then + abs(x) +end +)"); + + LUAU_REQUIRE_NO_ERRORS(result); +} + +TEST_CASE_FIXTURE(NonStrictTypeCheckerFixture, "if_then_no_else_err_in_cond") +{ + CheckResult result = checkNonStrict(R"( +local x : string +if abs(x) then + lower(x) +end +)"); + LUAU_REQUIRE_ERROR_COUNT(1, result); + NONSTRICT_REQUIRE_CHECKED_ERR(0, "abs", result); +} + +TEST_CASE_FIXTURE(NonStrictTypeCheckerFixture, "if_then_else_expr_should_warn") +{ + CheckResult result = checkNonStrict(R"( +local x : never +local y = if cond() then abs(x) else lower(x) +)"); + LUAU_REQUIRE_ERROR_COUNT(2, result); + NONSTRICT_REQUIRE_CHECKED_ERR(0, "abs", result); + NONSTRICT_REQUIRE_CHECKED_ERR(1, "lower", result); +} + +TEST_CASE_FIXTURE(NonStrictTypeCheckerFixture, "if_then_else_expr_doesnt_warn_else_branch") +{ + CheckResult result = checkNonStrict(R"( +local x : string = "hi" +local y = if cond() then abs(x) else lower(x) +)"); + LUAU_REQUIRE_ERROR_COUNT(1, result); + NONSTRICT_REQUIRE_CHECKED_ERR(0, "abs", result); +} + + +TEST_CASE_FIXTURE(NonStrictTypeCheckerFixture, "sequencing_errors") +{ + CheckResult result = checkNonStrict(R"( +function f(x) + abs(x) + lower(x) +end +)"); + LUAU_REQUIRE_ERROR_COUNT(2, result); + NONSTRICT_REQUIRE_CHECKED_ERR(0, "abs", result); + NONSTRICT_REQUIRE_CHECKED_ERR(1, "lower", result); +} + +TEST_CASE_FIXTURE(NonStrictTypeCheckerFixture, "sequencing_if_checked_call") +{ + CheckResult result = checkNonStrict(R"( +local x +if cond() then + x = 5 +else + x = nil +end +lower(x) +)"); + LUAU_REQUIRE_ERROR_COUNT(1, result); + NONSTRICT_REQUIRE_CHECKED_ERR(0, "lower", result); +} + +TEST_CASE_FIXTURE(NonStrictTypeCheckerFixture, "sequencing_unrelated_checked_calls") +{ + CheckResult result = checkNonStrict(R"( +function h(x, y) + abs(x) + lower(y) +end +)"); + + LUAU_REQUIRE_NO_ERRORS(result); +} + + TEST_SUITE_END(); diff --git a/tests/Set.test.cpp b/tests/Set.test.cpp index 4476452a..a70f4f61 100644 --- a/tests/Set.test.cpp +++ b/tests/Set.test.cpp @@ -60,4 +60,43 @@ TEST_CASE("erase_works_and_decreases_size") CHECK(!s1.contains(2)); } +TEST_CASE("iterate_over_set") +{ + Luau::Set s1{0}; + s1.insert(1); + s1.insert(2); + s1.insert(3); + REQUIRE(s1.size() == 3); + + int sum = 0; + + for (int e : s1) + sum += e; + + CHECK(sum == 6); +} + +TEST_CASE("iterate_over_set_skips_erased_elements") +{ + Luau::Set s1{0}; + s1.insert(1); + s1.insert(2); + s1.insert(3); + s1.insert(4); + s1.insert(5); + s1.insert(6); + REQUIRE(s1.size() == 6); + + s1.erase(2); + s1.erase(4); + s1.erase(6); + + int sum = 0; + + for (int e : s1) + sum += e; + + CHECK(sum == 9); +} + TEST_SUITE_END(); diff --git a/tests/Subtyping.test.cpp b/tests/Subtyping.test.cpp index d7120bca..cda1fbf5 100644 --- a/tests/Subtyping.test.cpp +++ b/tests/Subtyping.test.cpp @@ -10,6 +10,7 @@ #include "doctest.h" #include "Fixture.h" #include "RegisterCallbacks.h" + #include using namespace Luau; @@ -17,9 +18,38 @@ using namespace Luau; namespace Luau { +std::ostream& operator<<(std::ostream& lhs, const SubtypingVariance& variance) +{ + switch (variance) + { + case SubtypingVariance::Covariant: + return lhs << "covariant"; + case SubtypingVariance::Invariant: + return lhs << "invariant"; + case SubtypingVariance::Invalid: + return lhs << "*invalid*"; + } + + return lhs; +} + std::ostream& operator<<(std::ostream& lhs, const SubtypingReasoning& reasoning) { - return lhs << toString(reasoning.subPath) << " & 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; } }; // namespace Luau @@ -1105,20 +1135,6 @@ 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}}); @@ -1126,10 +1142,9 @@ TEST_CASE_FIXTURE(SubtypeFixture, "table_property") SubtypingResult result = isSubtype(subTy, superTy); CHECK(!result.isSubtype); - CHECK(result.reasoning == std::vector{SubtypingReasoning{ - /* subPath */ Path(TypePath::Property("X")), + CHECK(result.reasoning == std::vector{SubtypingReasoning{/* subPath */ Path(TypePath::Property("X")), /* superPath */ Path(TypePath::Property("X")), - }}); + /* variance */ SubtypingVariance::Invariant}}); } TEST_CASE_FIXTURE(SubtypeFixture, "table_indexers") @@ -1142,10 +1157,12 @@ TEST_CASE_FIXTURE(SubtypeFixture, "table_indexers") CHECK(result.reasoning == std::vector{SubtypingReasoning{ /* subPath */ Path(TypePath::TypeField::IndexLookup), /* superPath */ Path(TypePath::TypeField::IndexLookup), + /* variance */ SubtypingVariance::Invariant, }, SubtypingReasoning{ /* subPath */ Path(TypePath::TypeField::IndexResult), /* superPath */ Path(TypePath::TypeField::IndexResult), + /* variance */ SubtypingVariance::Invariant, }}); } @@ -1211,6 +1228,7 @@ TEST_CASE_FIXTURE(SubtypeFixture, "nested_table_properties") 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(), + /* variance */ SubtypingVariance::Invariant, }}); } @@ -1252,8 +1270,10 @@ TEST_CASE_FIXTURE(SubtypeFixture, "multiple_reasonings") 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"))}, + SubtypingReasoning{/* subPath */ Path(TypePath::Property("X")), /* superPath */ Path(TypePath::Property("X")), + /* variance */ SubtypingVariance::Invariant}, + SubtypingReasoning{/* subPath */ Path(TypePath::Property("Y")), /* superPath */ Path(TypePath::Property("Y")), + /* variance */ SubtypingVariance::Invariant}, }); } diff --git a/tests/ToString.test.cpp b/tests/ToString.test.cpp index 00c3c737..be9fc75d 100644 --- a/tests/ToString.test.cpp +++ b/tests/ToString.test.cpp @@ -938,7 +938,7 @@ TEST_CASE_FIXTURE(Fixture, "tostring_error_mismatch") //clang-format off std::string expected = (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 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 exactly number)" : R"(Type '{ a: number, b: string, c: { d: string } }' diff --git a/tests/TypeInfer.aliases.test.cpp b/tests/TypeInfer.aliases.test.cpp index 199b1b22..598e2dd9 100644 --- a/tests/TypeInfer.aliases.test.cpp +++ b/tests/TypeInfer.aliases.test.cpp @@ -198,8 +198,7 @@ TEST_CASE_FIXTURE(Fixture, "generic_aliases") )"); LUAU_REQUIRE_ERROR_COUNT(1, result); - const std::string expected = - R"(Type 'bad' could not be converted into 'T'; at ["v"], string is not a subtype of number)"; + const std::string expected = R"(Type 'bad' could not be converted into 'T'; at ["v"], string is not exactly number)"; CHECK(result.errors[0].location == Location{{4, 31}, {4, 44}}); CHECK_EQ(expected, toString(result.errors[0])); } @@ -218,8 +217,7 @@ TEST_CASE_FIXTURE(Fixture, "dependent_generic_aliases") )"); LUAU_REQUIRE_ERROR_COUNT(1, result); - const std::string expected = - R"(Type 'bad' could not be converted into 'U'; at ["t"]["v"], string is not a subtype of number)"; + const std::string expected = R"(Type 'bad' could not be converted into 'U'; at ["t"]["v"], string is not exactly number)"; CHECK(result.errors[0].location == Location{{4, 31}, {4, 52}}); CHECK_EQ(expected, toString(result.errors[0])); diff --git a/tests/TypeInfer.anyerror.test.cpp b/tests/TypeInfer.anyerror.test.cpp index 1e3db820..5d6b9b16 100644 --- a/tests/TypeInfer.anyerror.test.cpp +++ b/tests/TypeInfer.anyerror.test.cpp @@ -50,7 +50,15 @@ TEST_CASE_FIXTURE(Fixture, "for_in_loop_iterator_returns_any2") LUAU_REQUIRE_NO_ERRORS(result); - CHECK_EQ("any", toString(requireType("a"))); + if (FFlag::DebugLuauDeferredConstraintResolution) + { + // Bug: We do not simplify at the right time + CHECK_EQ("any?", toString(requireType("a"))); + } + else + { + CHECK_EQ("any", toString(requireType("a"))); + } } TEST_CASE_FIXTURE(Fixture, "for_in_loop_iterator_is_any") diff --git a/tests/TypeInfer.classes.test.cpp b/tests/TypeInfer.classes.test.cpp index a6d3fa5c..b53f60f0 100644 --- a/tests/TypeInfer.classes.test.cpp +++ b/tests/TypeInfer.classes.test.cpp @@ -111,7 +111,7 @@ TEST_CASE_FIXTURE(ClassFixture, "we_can_report_when_someone_is_trying_to_use_a_t )"); LUAU_REQUIRE_ERROR_COUNT(1, result); - TypeMismatch* tm = get(result.errors[0]); + TypeMismatch* tm = get(result.errors.at(0)); REQUIRE(tm != nullptr); CHECK_EQ("Oopsies", toString(tm->givenType)); @@ -186,7 +186,7 @@ TEST_CASE_FIXTURE(ClassFixture, "warn_when_prop_almost_matches") LUAU_REQUIRE_ERROR_COUNT(1, result); - auto err = get(result.errors[0]); + auto err = get(result.errors.at(0)); REQUIRE(err != nullptr); REQUIRE_EQ(1, err->candidates.size()); @@ -290,7 +290,7 @@ TEST_CASE_FIXTURE(ClassFixture, "table_properties_are_invariant") )"); LUAU_REQUIRE_ERROR_COUNT(2, result); - CHECK_EQ(6, result.errors[0].location.begin.line); + CHECK_EQ(6, result.errors.at(0).location.begin.line); CHECK_EQ(13, result.errors[1].location.begin.line); } @@ -313,7 +313,7 @@ TEST_CASE_FIXTURE(ClassFixture, "table_indexers_are_invariant") )"); LUAU_REQUIRE_ERROR_COUNT(2, result); - CHECK_EQ(6, result.errors[0].location.begin.line); + CHECK_EQ(6, result.errors.at(0).location.begin.line); CHECK_EQ(13, result.errors[1].location.begin.line); } @@ -331,7 +331,7 @@ TEST_CASE_FIXTURE(ClassFixture, "table_class_unification_reports_sane_errors_for )"); LUAU_REQUIRE_ERROR_COUNT(2, result); - REQUIRE_EQ("Key 'w' not found in class 'Vector2'", toString(result.errors[0])); + REQUIRE_EQ("Key 'w' not found in class 'Vector2'", toString(result.errors.at(0))); REQUIRE_EQ("Key 'x' not found in class 'Vector2'. Did you mean 'X'?", toString(result.errors[1])); } @@ -345,7 +345,7 @@ TEST_CASE_FIXTURE(ClassFixture, "class_unification_type_mismatch_is_correct_orde LUAU_REQUIRE_ERROR_COUNT(2, result); - REQUIRE_EQ("Type 'BaseClass' could not be converted into 'number'", toString(result.errors[0])); + REQUIRE_EQ("Type 'BaseClass' could not be converted into 'number'", toString(result.errors.at(0))); REQUIRE_EQ("Type 'number' could not be converted into 'BaseClass'", toString(result.errors[1])); } @@ -359,7 +359,7 @@ b.X = 2 -- real Vector2.X is also read-only )"); LUAU_REQUIRE_ERROR_COUNT(4, result); - CHECK_EQ("Value of type 'Vector2?' could be nil", toString(result.errors[0])); + CHECK_EQ("Value of type 'Vector2?' could be nil", toString(result.errors.at(0))); CHECK_EQ("Value of type 'Vector2?' could be nil", toString(result.errors[1])); CHECK_EQ("Key 'Z' not found in class 'Vector2'", toString(result.errors[2])); CHECK_EQ("Value of type 'Vector2?' could be nil", toString(result.errors[3])); @@ -385,7 +385,7 @@ b(a) caused by: Property 'Y' is not compatible. Type 'number' could not be converted into 'string')"; - CHECK_EQ(expected, toString(result.errors[0])); + CHECK_EQ(expected, toString(result.errors.at(0))); } TEST_CASE_FIXTURE(ClassFixture, "class_type_mismatch_with_name_conflict") @@ -397,7 +397,7 @@ local a: ChildClass = i )"); LUAU_REQUIRE_ERROR_COUNT(1, result); - CHECK_EQ("Type 'ChildClass' from 'Test' could not be converted into 'ChildClass' from 'MainModule'", toString(result.errors[0])); + CHECK_EQ("Type 'ChildClass' from 'Test' could not be converted into 'ChildClass' from 'MainModule'", toString(result.errors.at(0))); } TEST_CASE_FIXTURE(ClassFixture, "intersections_of_unions_of_classes") @@ -433,7 +433,7 @@ TEST_CASE_FIXTURE(ClassFixture, "index_instance_property") )"); LUAU_REQUIRE_ERROR_COUNT(1, result); - CHECK_EQ("Attempting a dynamic property access on type 'BaseClass' is unsafe and may cause exceptions at runtime", toString(result.errors[0])); + CHECK_EQ("Attempting a dynamic property access on type 'BaseClass' is unsafe and may cause exceptions at runtime", toString(result.errors.at(0))); } TEST_CASE_FIXTURE(ClassFixture, "index_instance_property_nonstrict") @@ -455,16 +455,22 @@ TEST_CASE_FIXTURE(ClassFixture, "type_mismatch_invariance_required_for_error") type A = { x: ChildClass } type B = { x: BaseClass } -local a: A +local a: A = { x = ChildClass.New() } local b: B = a )"); LUAU_REQUIRE_ERRORS(result); - const std::string expected = R"(Type 'A' could not be converted into 'B' + + if (FFlag::DebugLuauDeferredConstraintResolution) + CHECK(toString(result.errors.at(0)) == "Type 'a' could not be converted into 'B'; at [\"x\"], ChildClass is not exactly BaseClass"); + else + { + const std::string expected = R"(Type 'A' could not be converted into 'B' caused by: Property 'x' is not compatible. Type 'ChildClass' could not be converted into 'BaseClass' in an invariant context)"; - CHECK_EQ(expected, toString(result.errors[0])); + CHECK_EQ(expected, toString(result.errors.at(0))); + } } TEST_CASE_FIXTURE(ClassFixture, "callable_classes") @@ -551,7 +557,7 @@ TEST_CASE_FIXTURE(ClassFixture, "indexable_classes") CHECK_EQ( - toString(result.errors[0]), "Type 'boolean' could not be converted into 'number | string'; none of the union options are compatible"); + toString(result.errors.at(0)), "Type 'boolean' could not be converted into 'number | string'; none of the union options are compatible"); } { CheckResult result = check(R"( @@ -560,7 +566,7 @@ TEST_CASE_FIXTURE(ClassFixture, "indexable_classes") )"); CHECK_EQ( - toString(result.errors[0]), "Type 'boolean' could not be converted into 'number | string'; none of the union options are compatible"); + toString(result.errors.at(0)), "Type 'boolean' could not be converted into 'number | string'; none of the union options are compatible"); } // Test type checking for the return type of the indexer (i.e. a number) @@ -569,14 +575,14 @@ TEST_CASE_FIXTURE(ClassFixture, "indexable_classes") local x : IndexableClass x.key = "string value" )"); - CHECK_EQ(toString(result.errors[0]), "Type 'string' could not be converted into 'number'"); + CHECK_EQ(toString(result.errors.at(0)), "Type 'string' could not be converted into 'number'"); } { CheckResult result = check(R"( local x : IndexableClass local str : string = x.key )"); - CHECK_EQ(toString(result.errors[0]), "Type 'number' could not be converted into 'string'"); + CHECK_EQ(toString(result.errors.at(0)), "Type 'number' could not be converted into 'string'"); } // Check that we string key are rejected if the indexer's key type is not compatible with string @@ -593,9 +599,9 @@ TEST_CASE_FIXTURE(ClassFixture, "indexable_classes") x["key"] = 1 )"); if (FFlag::DebugLuauDeferredConstraintResolution) - CHECK_EQ(toString(result.errors[0]), "Key 'key' not found in class 'IndexableNumericKeyClass'"); + CHECK_EQ(toString(result.errors.at(0)), "Key 'key' not found in class 'IndexableNumericKeyClass'"); else - CHECK_EQ(toString(result.errors[0]), "Type 'string' could not be converted into 'number'"); + CHECK_EQ(toString(result.errors.at(0)), "Type 'string' could not be converted into 'number'"); } { CheckResult result = check(R"( @@ -603,14 +609,14 @@ TEST_CASE_FIXTURE(ClassFixture, "indexable_classes") local str : string x[str] = 1 -- Index with a non-const string )"); - CHECK_EQ(toString(result.errors[0]), "Type 'string' could not be converted into 'number'"); + CHECK_EQ(toString(result.errors.at(0)), "Type 'string' could not be converted into 'number'"); } { CheckResult result = check(R"( local x : IndexableNumericKeyClass local y = x.key )"); - CHECK_EQ(toString(result.errors[0]), "Key 'key' not found in class 'IndexableNumericKeyClass'"); + CHECK_EQ(toString(result.errors.at(0)), "Key 'key' not found in class 'IndexableNumericKeyClass'"); } { CheckResult result = check(R"( @@ -618,9 +624,9 @@ TEST_CASE_FIXTURE(ClassFixture, "indexable_classes") local y = x["key"] )"); if (FFlag::DebugLuauDeferredConstraintResolution) - CHECK_EQ(toString(result.errors[0]), "Key 'key' not found in class 'IndexableNumericKeyClass'"); + CHECK_EQ(toString(result.errors.at(0)), "Key 'key' not found in class 'IndexableNumericKeyClass'"); else - CHECK_EQ(toString(result.errors[0]), "Type 'string' could not be converted into 'number'"); + CHECK_EQ(toString(result.errors.at(0)), "Type 'string' could not be converted into 'number'"); } { CheckResult result = check(R"( @@ -628,7 +634,7 @@ TEST_CASE_FIXTURE(ClassFixture, "indexable_classes") local str : string local y = x[str] -- Index with a non-const string )"); - CHECK_EQ(toString(result.errors[0]), "Type 'string' could not be converted into 'number'"); + CHECK_EQ(toString(result.errors.at(0)), "Type 'string' could not be converted into 'number'"); } } diff --git a/tests/TypeInfer.generics.test.cpp b/tests/TypeInfer.generics.test.cpp index 1fbd9008..bff86b03 100644 --- a/tests/TypeInfer.generics.test.cpp +++ b/tests/TypeInfer.generics.test.cpp @@ -725,14 +725,21 @@ y.a.c = y )"); LUAU_REQUIRE_ERRORS(result); - const std::string expected = R"(Type 'y' could not be converted into 'T' + + if (FFlag::DebugLuauDeferredConstraintResolution) + CHECK(toString(result.errors.at(0)) == + R"(Type 'x' could not be converted into 'T'; type x["a"]["c"] (nil) is not exactly T["a"]["c"][0] (T))"); + else + { + const std::string expected = R"(Type 'y' could not be converted into 'T' caused by: Property 'a' is not compatible. Type '{ c: T?, d: number }' could not be converted into 'U' caused by: Property 'd' is not compatible. Type 'number' could not be converted into 'string' in an invariant context)"; - CHECK_EQ(expected, toString(result.errors[0])); + CHECK_EQ(expected, toString(result.errors[0])); + } } TEST_CASE_FIXTURE(Fixture, "generic_type_pack_unification1") diff --git a/tests/TypeInfer.intersectionTypes.test.cpp b/tests/TypeInfer.intersectionTypes.test.cpp index 6892c78f..2023a838 100644 --- a/tests/TypeInfer.intersectionTypes.test.cpp +++ b/tests/TypeInfer.intersectionTypes.test.cpp @@ -529,7 +529,7 @@ could not be converted into TEST_CASE_FIXTURE(Fixture, "intersection_of_tables_with_top_properties") { CheckResult result = check(R"( - local x : { p : number?, q : any } & { p : unknown, q : string? } + local x : { p : number?, q : any } & { p : unknown, q : string? } = { p = 123, q = "foo" } local y : { p : number?, q : string? } = x -- OK local z : { p : string?, q : number? } = x -- Not OK )"); diff --git a/tests/TypeInfer.modules.test.cpp b/tests/TypeInfer.modules.test.cpp index 3c126c7e..31f5f7f2 100644 --- a/tests/TypeInfer.modules.test.cpp +++ b/tests/TypeInfer.modules.test.cpp @@ -410,12 +410,18 @@ local b: B.T = a )"; CheckResult result = frontend.check("game/C"); - const std::string expected = R"(Type 'T' from 'game/A' could not be converted into 'T' from 'game/B' + LUAU_REQUIRE_ERROR_COUNT(1, result); + + if (FFlag::DebugLuauDeferredConstraintResolution) + CHECK(toString(result.errors.at(0)) == "Type 'a' could not be converted into 'T'; at [\"x\"], number is not exactly string"); + else + { + const std::string expected = R"(Type 'T' from 'game/A' could not be converted into 'T' from 'game/B' caused by: Property 'x' is not compatible. Type 'number' could not be converted into 'string' in an invariant context)"; - LUAU_REQUIRE_ERROR_COUNT(1, result); - CHECK_EQ(expected, toString(result.errors[0])); + CHECK_EQ(expected, toString(result.errors[0])); + } } TEST_CASE_FIXTURE(BuiltinsFixture, "module_type_conflict_instantiated") @@ -445,12 +451,18 @@ local b: B.T = a )"; CheckResult result = frontend.check("game/D"); - const std::string expected = R"(Type 'T' from 'game/B' could not be converted into 'T' from 'game/C' + LUAU_REQUIRE_ERROR_COUNT(1, result); + + if (FFlag::DebugLuauDeferredConstraintResolution) + CHECK(toString(result.errors.at(0)) == "Type 'a' could not be converted into 'T'; at [\"x\"], number is not exactly string"); + else + { + const std::string expected = R"(Type 'T' from 'game/B' could not be converted into 'T' from 'game/C' caused by: Property 'x' is not compatible. Type 'number' could not be converted into 'string' in an invariant context)"; - LUAU_REQUIRE_ERROR_COUNT(1, result); - CHECK_EQ(expected, toString(result.errors[0])); + CHECK_EQ(expected, toString(result.errors[0])); + } } TEST_CASE_FIXTURE(BuiltinsFixture, "constrained_anyification_clone_immutable_types") diff --git a/tests/TypeInfer.refinements.test.cpp b/tests/TypeInfer.refinements.test.cpp index cba1f37e..f0c70261 100644 --- a/tests/TypeInfer.refinements.test.cpp +++ b/tests/TypeInfer.refinements.test.cpp @@ -1939,8 +1939,17 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "refine_unknown_to_table") LUAU_REQUIRE_NO_ERRORS(result); - CHECK_EQ("unknown", toString(requireType("idx"))); - CHECK_EQ("unknown", toString(requireType("val"))); + if (FFlag::DebugLuauDeferredConstraintResolution) + { + // Bug: We do not simplify at the right time + CHECK_EQ("unknown?", toString(requireType("idx"))); + CHECK_EQ("unknown?", toString(requireType("val"))); + } + else + { + CHECK_EQ("unknown", toString(requireType("idx"))); + CHECK_EQ("unknown", toString(requireType("val"))); + } } TEST_CASE_FIXTURE(BuiltinsFixture, "conditional_refinement_should_stay_error_suppressing") diff --git a/tests/TypeInfer.singletons.test.cpp b/tests/TypeInfer.singletons.test.cpp index 1bc6b380..6fda6a2a 100644 --- a/tests/TypeInfer.singletons.test.cpp +++ b/tests/TypeInfer.singletons.test.cpp @@ -367,8 +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)" - "\n\ttype a[\"success\"] (false) is not a subtype of Err | Ok[0][\"success\"] (true)"; + "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 exactly 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 8e32a6a7..d3c15728 100644 --- a/tests/TypeInfer.tables.test.cpp +++ b/tests/TypeInfer.tables.test.cpp @@ -2147,16 +2147,22 @@ TEST_CASE_FIXTURE(Fixture, "error_detailed_prop") type A = { x: number, y: number } type B = { x: number, y: string } -local a: A +local a: A = { x = 123, y = 456 } local b: B = a )"); LUAU_REQUIRE_ERRORS(result); - const std::string expected = R"(Type 'A' could not be converted into 'B' + + if (FFlag::DebugLuauDeferredConstraintResolution) + CHECK(toString(result.errors.at(0)) == R"(Type 'a' could not be converted into 'B'; at ["y"], number is not exactly string)"); + else + { + const std::string expected = R"(Type 'A' could not be converted into 'B' caused by: Property 'y' is not compatible. Type 'number' could not be converted into 'string' in an invariant context)"; - CHECK_EQ(expected, toString(result.errors[0])); + CHECK_EQ(expected, toString(result.errors[0])); + } } TEST_CASE_FIXTURE(Fixture, "error_detailed_prop_nested") @@ -2168,19 +2174,25 @@ type BS = { x: number, y: string } type A = { a: boolean, b: AS } type B = { a: boolean, b: BS } -local a: A +local a: A = { a = false, b = { x = 123, y = 456 } } local b: B = a )"); LUAU_REQUIRE_ERRORS(result); - const std::string expected = R"(Type 'A' could not be converted into 'B' + + if (FFlag::DebugLuauDeferredConstraintResolution) + CHECK(toString(result.errors.at(0)) == R"(Type 'a' could not be converted into 'B'; at ["b"]["y"], number is not exactly string)"); + else + { + const std::string expected = R"(Type 'A' could not be converted into 'B' caused by: Property 'b' is not compatible. Type 'AS' could not be converted into 'BS' caused by: Property 'y' is not compatible. Type 'number' could not be converted into 'string' in an invariant context)"; - CHECK_EQ(expected, toString(result.errors[0])); + CHECK_EQ(expected, toString(result.errors[0])); + } } TEST_CASE_FIXTURE(BuiltinsFixture, "error_detailed_metatable_prop") @@ -3945,9 +3957,9 @@ TEST_CASE_FIXTURE(Fixture, "identify_all_problematic_table_fields") 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"; + std::string expected = "Type 'a' could not be converted into 'T'; at [\"a\"], string is not exactly number" + "\n\tat [\"b\"], boolean is not exactly string" + "\n\tat [\"c\"], number is not exactly boolean"; CHECK(toString(result.errors[0]) == expected); } diff --git a/tests/TypeInfer.test.cpp b/tests/TypeInfer.test.cpp index 5af34930..c34be936 100644 --- a/tests/TypeInfer.test.cpp +++ b/tests/TypeInfer.test.cpp @@ -28,8 +28,7 @@ TEST_CASE_FIXTURE(Fixture, "tc_hello_world") CheckResult result = check("local a = 7"); LUAU_REQUIRE_NO_ERRORS(result); - TypeId aType = requireType("a"); - CHECK_EQ(getPrimitiveType(aType), PrimitiveType::Number); + CHECK("number" == toString(requireType("a"))); } TEST_CASE_FIXTURE(Fixture, "tc_propagation") @@ -44,21 +43,39 @@ TEST_CASE_FIXTURE(Fixture, "tc_propagation") TEST_CASE_FIXTURE(Fixture, "tc_error") { CheckResult result = check("local a = 7 local b = 'hi' a = b"); - LUAU_REQUIRE_ERROR_COUNT(1, result); - CHECK_EQ( - result.errors[0], (TypeError{Location{Position{0, 35}, Position{0, 36}}, TypeMismatch{builtinTypes->numberType, builtinTypes->stringType}})); + if (FFlag::DebugLuauDeferredConstraintResolution) + { + LUAU_REQUIRE_NO_ERRORS(result); + CHECK("number | string" == toString(requireType("a"))); + } + else + { + LUAU_REQUIRE_ERROR_COUNT(1, result); + + CHECK_EQ( + result.errors[0], (TypeError{Location{Position{0, 35}, Position{0, 36}}, TypeMismatch{builtinTypes->numberType, builtinTypes->stringType}})); + } } TEST_CASE_FIXTURE(Fixture, "tc_error_2") { CheckResult result = check("local a = 7 a = 'hi'"); - LUAU_REQUIRE_ERROR_COUNT(1, result); - CHECK_EQ(result.errors[0], (TypeError{Location{Position{0, 18}, Position{0, 22}}, TypeMismatch{ - requireType("a"), - builtinTypes->stringType, - }})); + if (FFlag::DebugLuauDeferredConstraintResolution) + { + LUAU_REQUIRE_NO_ERRORS(result); + CHECK("number | string" == toString(requireType("a"))); + } + else + { + LUAU_REQUIRE_ERROR_COUNT(1, result); + + CHECK_EQ(result.errors[0], (TypeError{Location{Position{0, 18}, Position{0, 22}}, TypeMismatch{ + requireType("a"), + builtinTypes->stringType, + }})); + } } TEST_CASE_FIXTURE(Fixture, "infer_locals_with_nil_value") @@ -66,8 +83,15 @@ TEST_CASE_FIXTURE(Fixture, "infer_locals_with_nil_value") CheckResult result = check("local f = nil; f = 'hello world'"); LUAU_REQUIRE_NO_ERRORS(result); - TypeId ty = requireType("f"); - CHECK_EQ(getPrimitiveType(ty), PrimitiveType::String); + if (FFlag::DebugLuauDeferredConstraintResolution) + { + CHECK("string?" == toString(requireType("f"))); + } + else + { + TypeId ty = requireType("f"); + CHECK_EQ(getPrimitiveType(ty), PrimitiveType::String); + } } TEST_CASE_FIXTURE(Fixture, "infer_locals_with_nil_value_2") @@ -93,8 +117,8 @@ TEST_CASE_FIXTURE(Fixture, "infer_locals_via_assignment_from_its_call_site") if (FFlag::DebugLuauDeferredConstraintResolution) { - CHECK("number | string" == toString(requireType("a"))); - CHECK("(number | string) -> ()" == toString(requireType("f"))); + CHECK("unknown" == toString(requireType("a"))); + CHECK("(unknown) -> ()" == toString(requireType("f"))); LUAU_REQUIRE_NO_ERRORS(result); } @@ -105,27 +129,6 @@ TEST_CASE_FIXTURE(Fixture, "infer_locals_via_assignment_from_its_call_site") CHECK_EQ("number", toString(requireType("a"))); } } -TEST_CASE_FIXTURE(Fixture, "interesting_local_type_inference_case") -{ - if (!FFlag::DebugLuauDeferredConstraintResolution) - return; - - ScopedFastFlag sff[] = { - {"DebugLuauDeferredConstraintResolution", true}, - }; - - CheckResult result = check(R"( - local a - function f(x) a = x end - f({x = 5}) - f({x = 5}) - )"); - - CHECK("{ x: number }" == toString(requireType("a"))); - CHECK("({ x: number }) -> ()" == toString(requireType("f"))); - - LUAU_REQUIRE_NO_ERRORS(result); -} TEST_CASE_FIXTURE(Fixture, "infer_in_nocheck_mode") { @@ -178,8 +181,16 @@ TEST_CASE_FIXTURE(Fixture, "if_statement") LUAU_REQUIRE_NO_ERRORS(result); - CHECK_EQ(*builtinTypes->stringType, *requireType("a")); - CHECK_EQ(*builtinTypes->numberType, *requireType("b")); + if (FFlag::DebugLuauDeferredConstraintResolution) + { + CHECK("string?" == toString(requireType("a"))); + CHECK("number?" == toString(requireType("b"))); + } + else + { + CHECK_EQ(*builtinTypes->stringType, *requireType("a")); + CHECK_EQ(*builtinTypes->numberType, *requireType("b")); + } } TEST_CASE_FIXTURE(Fixture, "statements_are_topologically_sorted") diff --git a/tests/TypeInfer.typestates.test.cpp b/tests/TypeInfer.typestates.test.cpp index cee36832..2f07c148 100644 --- a/tests/TypeInfer.typestates.test.cpp +++ b/tests/TypeInfer.typestates.test.cpp @@ -274,4 +274,45 @@ TEST_CASE_FIXTURE(TypeStateFixture, "then_branch_assigns_but_is_met_with_return_ CHECK("string?" == toString(requireType("y"))); } +TEST_CASE_FIXTURE(TypeStateFixture, "invalidate_type_refinements_upon_assignments") +{ + CheckResult result = check(R"( + type Ok = { tag: "ok", val: T } + type Err = { tag: "err", err: E } + type Result = Ok | Err + + local function f(res: Result) + assert(res.tag == "ok") + local tag: "ok", val: T = res.tag, res.val + res = { tag = "err" :: "err", err = (5 :: any) :: E } + local tag: "err", err: E = res.tag, res.err + end + )"); + + LUAU_REQUIRE_NO_ERRORS(result); +} + +TEST_CASE_FIXTURE(TypeStateFixture, "local_t_is_assigned_a_fresh_table_with_x_assigned_a_union_and_then_assert_restricts_actual_outflow_of_types") +{ + CheckResult result = check(R"( + local t = nil + + if math.random() > 0.5 then + t = {} + t.x = if math.random() > 0.5 then 5 else "hello" + assert(typeof(t.x) == "string") + else + t = {} + t.x = if math.random() > 0.5 then 7 else true + assert(typeof(t.x) == "boolean") + end + + local x = t.x + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + // CHECK("boolean | string" == toString(requireType("x"))); + CHECK("boolean | number | number | string" == toString(requireType("x"))); +} + TEST_SUITE_END(); diff --git a/tests/conformance/buffers.lua b/tests/conformance/buffers.lua index 1cf996da..5da2a688 100644 --- a/tests/conformance/buffers.lua +++ b/tests/conformance/buffers.lua @@ -586,6 +586,15 @@ local function misc(t16) buffer.writei32(b, #t16, 10) assert(buffer.readi32(b, 16) == 10) + + buffer.writeu8(b, 100, 0xff) + buffer.writeu8(b, 110, 0x80) + assert(buffer.readu32(b, 100) == 255) + assert(buffer.readu32(b, 110) == 128) + buffer.writeu16(b, 200, 0xffff) + buffer.writeu16(b, 210, 0x8000) + assert(buffer.readu32(b, 200) == 65535) + assert(buffer.readu32(b, 210) == 32768) end misc(table.create(16, 0)) diff --git a/tests/conformance/native.lua b/tests/conformance/native.lua index 08d458f9..63c6ff09 100644 --- a/tests/conformance/native.lua +++ b/tests/conformance/native.lua @@ -275,4 +275,22 @@ end assert(arrayIndexingSpecialNumbers1(1, 256, 65536) == 3456789) +function loopIteratorProtocol(a, t) + local sum = 0 + + do + local a, b, c, d, e, f, g = {}, {}, {}, {}, {}, {}, {} + end + + for k, v in ipairs(t) do + if k == 10 then sum += math.abs('-8') end + + sum += k + end + + return sum +end + +assert(loopIteratorProtocol(0, table.create(100, 5)) == 5058) + return('OK') diff --git a/tools/faillist.txt b/tools/faillist.txt index c421f2ce..61fb4f47 100644 --- a/tools/faillist.txt +++ b/tools/faillist.txt @@ -13,7 +13,6 @@ AutocompleteTest.autocomplete_string_singleton_equality AutocompleteTest.autocomplete_string_singleton_escape AutocompleteTest.autocomplete_string_singletons AutocompleteTest.do_wrong_compatible_nonself_calls -AutocompleteTest.frontend_use_correct_global_scope AutocompleteTest.no_incompatible_self_calls_on_class AutocompleteTest.string_singleton_in_if_statement AutocompleteTest.suggest_external_module_type @@ -165,7 +164,6 @@ GenericsTests.generic_argument_count_too_many GenericsTests.generic_factories GenericsTests.generic_functions_dont_cache_type_parameters GenericsTests.generic_functions_in_types -GenericsTests.generic_functions_should_be_memory_safe GenericsTests.generic_type_pack_parentheses GenericsTests.generic_type_pack_unification1 GenericsTests.generic_type_pack_unification2 @@ -234,7 +232,6 @@ Linter.DeprecatedApiFenv Linter.FormatStringTyped Linter.TableOperationsIndexer ModuleTests.clone_self_property -Negations.cofinite_strings_can_be_compared_for_equality Negations.negated_string_is_a_subtype_of_string NonstrictModeTests.inconsistent_module_return_types_are_ok NonstrictModeTests.infer_nullary_function @@ -286,8 +283,6 @@ RefinementTest.function_call_with_colon_after_refining_not_to_be_nil RefinementTest.impossible_type_narrow_is_not_an_error RefinementTest.index_on_a_refined_property RefinementTest.isa_type_refinement_must_be_known_ahead_of_time -RefinementTest.luau_polyfill_isindexkey_refine_conjunction -RefinementTest.luau_polyfill_isindexkey_refine_conjunction_variant RefinementTest.merge_should_be_fully_agnostic_of_hashmap_ordering RefinementTest.narrow_property_of_a_bounded_variable RefinementTest.nonoptional_type_can_narrow_to_nil_if_sense_is_true @@ -340,15 +335,12 @@ TableTests.dont_suggest_exact_match_keys TableTests.error_detailed_indexer_key TableTests.error_detailed_indexer_value TableTests.error_detailed_metatable_prop -TableTests.error_detailed_prop -TableTests.error_detailed_prop_nested TableTests.expected_indexer_from_table_union TableTests.expected_indexer_value_type_extra 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 @@ -420,7 +412,6 @@ TableTests.table_unifies_into_map TableTests.top_table_type TableTests.type_mismatch_on_massive_table_is_cut_short TableTests.unification_of_unions_in_a_self_referential_type -TableTests.unifying_tables_shouldnt_uaf1 TableTests.used_colon_instead_of_dot TableTests.used_dot_instead_of_colon TableTests.used_dot_instead_of_colon_but_correctly @@ -485,14 +476,10 @@ TypeInfer.follow_on_new_types_in_substitution TypeInfer.globals TypeInfer.globals2 TypeInfer.globals_are_banned_in_strict_mode -TypeInfer.if_statement TypeInfer.infer_assignment_value_types -TypeInfer.infer_assignment_value_types_mutable_lval TypeInfer.infer_locals_via_assignment_from_its_call_site -TypeInfer.infer_locals_with_nil_value TypeInfer.infer_through_group_expr TypeInfer.infer_type_assertion_value_type -TypeInfer.interesting_local_type_inference_case TypeInfer.no_infinite_loop_when_trying_to_unify_uh_this TypeInfer.no_stack_overflow_from_isoptional TypeInfer.promote_tail_type_packs @@ -500,8 +487,6 @@ TypeInfer.recursive_function_that_invokes_itself_with_a_refinement_of_its_parame TypeInfer.recursive_function_that_invokes_itself_with_a_refinement_of_its_parameter_2 TypeInfer.stringify_nested_unions_with_optionals TypeInfer.tc_after_error_recovery_no_replacement_name_in_error -TypeInfer.tc_error -TypeInfer.tc_error_2 TypeInfer.tc_if_else_expressions_expected_type_3 TypeInfer.type_infer_recursion_limit_no_ice TypeInfer.type_infer_recursion_limit_normalizer @@ -531,7 +516,6 @@ TypeInferClasses.intersections_of_unions_of_classes TypeInferClasses.optional_class_field_access_error TypeInferClasses.table_class_unification_reports_sane_errors_for_missing_properties TypeInferClasses.table_indexers_are_invariant -TypeInferClasses.type_mismatch_invariance_required_for_error TypeInferClasses.unions_of_intersections_of_classes TypeInferClasses.we_can_report_when_someone_is_trying_to_use_a_table_rather_than_a_class TypeInferFunctions.another_other_higher_order_function @@ -605,6 +589,7 @@ TypeInferLoops.for_in_loop_on_non_function TypeInferLoops.for_in_loop_with_custom_iterator TypeInferLoops.for_in_loop_with_incompatible_args_to_iterator TypeInferLoops.for_in_loop_with_next +TypeInferLoops.for_in_with_a_custom_iterator_should_type_check TypeInferLoops.for_in_with_an_iterator_of_type_any TypeInferLoops.for_in_with_generic_next TypeInferLoops.for_in_with_just_one_iterator_is_ok @@ -626,10 +611,7 @@ TypeInferLoops.varlist_declared_by_for_in_loop_should_be_free TypeInferLoops.while_loop TypeInferModules.bound_free_table_export_is_ok TypeInferModules.do_not_modify_imported_types -TypeInferModules.do_not_modify_imported_types_4 TypeInferModules.do_not_modify_imported_types_5 -TypeInferModules.module_type_conflict -TypeInferModules.module_type_conflict_instantiated TypeInferModules.require TypeInferModules.require_failed_module TypeInferOOP.CheckMethodsOfSealed @@ -680,7 +662,6 @@ TypeInferUnknownNever.index_on_union_of_tables_for_properties_that_is_sorta_neve TypeInferUnknownNever.length_of_never TypeInferUnknownNever.math_operators_and_never TypeInferUnknownNever.type_packs_containing_never_is_itself_uninhabitable -TypePackTests.fuzz_typepack_iter_follow_2 TypePackTests.pack_tail_unification_check TypePackTests.type_alias_backwards_compatible TypePackTests.type_alias_default_type_errors @@ -699,6 +680,8 @@ TypeSingletons.table_properties_singleton_strings TypeSingletons.table_properties_type_error_escapes TypeSingletons.widen_the_supertype_if_it_is_free_and_subtype_has_singleton TypeSingletons.widening_happens_almost_everywhere +TypeStatesTest.invalidate_type_refinements_upon_assignments +TypeStatesTest.local_t_is_assigned_a_fresh_table_with_x_assigned_a_union_and_then_assert_restricts_actual_outflow_of_types UnionTypes.disallow_less_specific_assign UnionTypes.disallow_less_specific_assign2 UnionTypes.error_detailed_optional diff --git a/stats/compiler-stats.py b/tools/heuristicstat.py similarity index 100% rename from stats/compiler-stats.py rename to tools/heuristicstat.py diff --git a/tools/test_dcr.py b/tools/test_dcr.py index 30f8a310..3598b02c 100644 --- a/tools/test_dcr.py +++ b/tools/test_dcr.py @@ -113,6 +113,12 @@ def main(): action="store_true", help="Run the tests with read-write properties enabled.", ) + parser.add_argument( + "--ts", + dest="suite", + action="store", + help="Only run a specific suite." + ) parser.add_argument("--randomize", action="store_true", help="Pick a random seed") @@ -139,6 +145,9 @@ def main(): elif args.randomize: commandLine.append("--randomize") + if args.suite: + commandLine.append(f'--ts={args.suite}') + print_stderr(">", " ".join(commandLine)) p = sp.Popen( @@ -146,6 +155,8 @@ def main(): stdout=sp.PIPE, ) + assert p.stdout + handler = Handler(failList) if args.dump: From ae530518143c7215f046122faf49b89d9c2d368e Mon Sep 17 00:00:00 2001 From: JohnnyMorganz Date: Tue, 21 Nov 2023 17:12:18 +0100 Subject: [PATCH 10/19] Include module name for definitions files (#861) Closes #818 We set the `moduleName` of the source module to the provided `packageName`. This then populates the relevant `definitionModuleName`'s on CTV/FTV/TTVs, so it allows us to lookup where they originated from. The one place I can see this having an impact inside of Luau code is the following: https://github.com/Roblox/luau/blob/1fa8311a188a932233d2ce2bc6412e5bab37c035/Analysis/src/Error.cpp#L70-L90 Which will potentially change they way error messages are formatted. Should there be a way to exclude definition files when calling `getDefinitionModuleName`, so we can preserve this behaviour? --- Analysis/src/Frontend.cpp | 6 ++++++ tests/TypeInfer.definitions.test.cpp | 23 +++++++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/Analysis/src/Frontend.cpp b/Analysis/src/Frontend.cpp index 710e3699..125f2457 100644 --- a/Analysis/src/Frontend.cpp +++ b/Analysis/src/Frontend.cpp @@ -38,6 +38,7 @@ LUAU_FASTFLAGVARIABLE(DebugLuauLogSolverToJson, false) LUAU_FASTFLAGVARIABLE(DebugLuauReadWriteProperties, false) LUAU_FASTFLAGVARIABLE(LuauTypecheckLimitControls, false) LUAU_FASTFLAGVARIABLE(CorrectEarlyReturnInMarkDirty, false) +LUAU_FASTFLAGVARIABLE(LuauDefinitionFileSetModuleName, false) namespace Luau { @@ -165,6 +166,11 @@ LoadDefinitionFileResult Frontend::loadDefinitionFile(GlobalTypes& globals, Scop LUAU_TIMETRACE_SCOPE("loadDefinitionFile", "Frontend"); Luau::SourceModule sourceModule; + if (FFlag::LuauDefinitionFileSetModuleName) + { + sourceModule.name = packageName; + sourceModule.humanReadableName = packageName; + } Luau::ParseResult parseResult = parseSourceForModule(source, sourceModule, captureComments); if (parseResult.errors.size() > 0) return LoadDefinitionFileResult{false, parseResult, sourceModule, nullptr}; diff --git a/tests/TypeInfer.definitions.test.cpp b/tests/TypeInfer.definitions.test.cpp index 86f619fd..30153280 100644 --- a/tests/TypeInfer.definitions.test.cpp +++ b/tests/TypeInfer.definitions.test.cpp @@ -441,4 +441,27 @@ TEST_CASE_FIXTURE(Fixture, "class_definitions_reference_other_classes") REQUIRE(result.success); } +TEST_CASE_FIXTURE(Fixture, "definition_file_has_source_module_name_set") +{ + ScopedFastFlag sff{"LuauDefinitionFileSetModuleName", true}; + + LoadDefinitionFileResult result = loadDefinition(R"( + declare class Foo + end + )"); + + REQUIRE(result.success); + + CHECK_EQ(result.sourceModule.name, "@test"); + CHECK_EQ(result.sourceModule.humanReadableName, "@test"); + + std::optional fooTy = frontend.globals.globalScope->lookupType("Foo"); + REQUIRE(fooTy); + + const ClassType* ctv = get(fooTy->type); + + REQUIRE(ctv); + CHECK_EQ(ctv->definitionModuleName, "@test"); +} + TEST_SUITE_END(); From 1cda8304bf085b955109f9cc32cd51b581cdff54 Mon Sep 17 00:00:00 2001 From: vegorov-rbx <75688451+vegorov-rbx@users.noreply.github.com> Date: Tue, 21 Nov 2023 08:28:54 -0800 Subject: [PATCH 11/19] Add missing include file to BytecodeSummary.h (#1110) --- CodeGen/include/Luau/BytecodeSummary.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CodeGen/include/Luau/BytecodeSummary.h b/CodeGen/include/Luau/BytecodeSummary.h index cfdd5f84..648c0957 100644 --- a/CodeGen/include/Luau/BytecodeSummary.h +++ b/CodeGen/include/Luau/BytecodeSummary.h @@ -7,6 +7,8 @@ #include #include +#include + struct lua_State; struct Proto; From 7fb7f4382dd4fde31e0e044ef3a9b4f63646bd96 Mon Sep 17 00:00:00 2001 From: Arseny Kapoulkine Date: Mon, 27 Nov 2023 03:24:57 -0800 Subject: [PATCH 12/19] Use ubuntu-latest on GHA when possible (#1112) We need ubuntu-20.04 for coverage analysis (clang after 10 doesn't seem to properly interact with gcov version used for codecov) and for releases (to produce binaries targeting earlier glibc). However, we still should be verifying that Luau builds on latest, because newer toolchains have stricter standard library headers and/or warnings; without this we're at risk of constantly regressing the build for packaging or external applications. Note that release.yml can probably just be deleted but for now we simply adjust it. --- .github/workflows/build.yml | 6 +++--- .github/workflows/new-release.yml | 6 +++--- .github/workflows/release.yml | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b278c437..86284d74 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -20,7 +20,7 @@ jobs: unix: strategy: matrix: - os: [{name: ubuntu, version: ubuntu-20.04}, {name: macos, version: macos-latest}] + os: [{name: ubuntu, version: ubuntu-latest}, {name: macos, version: macos-latest}] name: ${{matrix.os.name}} runs-on: ${{matrix.os.version}} steps: @@ -83,7 +83,7 @@ jobs: Debug/luau-compile tests/conformance/assert.lua coverage: - runs-on: ubuntu-20.04 + runs-on: ubuntu-20.04 # needed for clang++-10 to avoid gcov compatibility issues steps: - uses: actions/checkout@v2 - name: install @@ -99,7 +99,7 @@ jobs: token: ${{ secrets.CODECOV_TOKEN }} web: - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest steps: - uses: actions/checkout@v1 - uses: actions/checkout@v2 diff --git a/.github/workflows/new-release.yml b/.github/workflows/new-release.yml index 52b2eb0c..078a18f9 100644 --- a/.github/workflows/new-release.yml +++ b/.github/workflows/new-release.yml @@ -12,7 +12,7 @@ permissions: jobs: create-release: - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest outputs: upload_url: ${{ steps.create_release.outputs.upload_url }} steps: @@ -29,7 +29,7 @@ jobs: build: needs: ["create-release"] strategy: - matrix: + matrix: # using ubuntu-20.04 to build a Linux binary targeting older glibc to improve compatibility os: [{name: ubuntu, version: ubuntu-20.04}, {name: macos, version: macos-latest}, {name: windows, version: windows-latest}] name: ${{matrix.os.name}} runs-on: ${{matrix.os.version}} @@ -56,7 +56,7 @@ jobs: web: needs: ["create-release"] - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest steps: - uses: actions/checkout@v1 - uses: actions/checkout@v2 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f796a7c1..7d8a5c44 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,7 +13,7 @@ on: jobs: build: strategy: - matrix: + matrix: # using ubuntu-20.04 to build a Linux binary targeting older glibc to improve compatibility os: [{name: ubuntu, version: ubuntu-20.04}, {name: macos, version: macos-latest}, {name: windows, version: windows-latest}] name: ${{matrix.os.name}} runs-on: ${{matrix.os.version}} @@ -35,7 +35,7 @@ jobs: path: Release\luau*.exe web: - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest steps: - uses: actions/checkout@v1 - uses: actions/checkout@v2 From 89b437bb4edd63f4ee6cca3f5dcfcd734b5c3209 Mon Sep 17 00:00:00 2001 From: Arseny Kapoulkine Date: Tue, 28 Nov 2023 07:35:01 -0800 Subject: [PATCH 13/19] Add SUBRK and DIVRK bytecode instructions to bytecode v5 (#1115) Right now, we can compile R\*K for all arithmetic instructions, but K\*R gets compiled into two instructions (LOADN/LOADK + arithmetic opcode). This is problematic since it leads to reduced performance for some code. However, we'd like to avoid adding reverse variants of ADDK et al for all opcodes to avoid the increase in I$ footprint for interpreter. Looking at the arithmetic instructions, % and // don't have interesting use cases for K\*V; ^ is sometimes used with constant on the left hand side but this would need to call pow() by necessity in all cases so it would be slow regardless of the dispatch overhead. This leaves the four basic arithmetic operations. For + and \*, we can implement a compiler-side optimization in the future that transforms K\*R to R\*K automatically. This could either be done unconditionally at -O2, or conditionally based on the type of the value (driven by type annotations / inference) -- this technically changes behavior in presence of metamethods, although it might be sensible to just always do this because non-commutative +/* are evil. However, for - and / it is impossible for the compiler to optimize this in the future, so we need dedicated opcodes. This only increases the interpreter size by ~300 bytes (~1.5%) on X64. This makes spectral-norm and math-partial-sums 6% faster; maybe more importantly, voxelgen gets 1.5% faster (so this change does have real-world impact). To avoid the proliferation of bytecode versions this change piggybacks on the bytecode version bump that was just made in 604 for vector constants; we would still be able to enable these independently but we'll consider v5 complete when both are enabled. Related: #626 --------- Co-authored-by: vegorov-rbx <75688451+vegorov-rbx@users.noreply.github.com> --- CodeGen/include/Luau/IrVisitUseDef.h | 5 ++ CodeGen/src/EmitCommonX64.cpp | 4 +- CodeGen/src/EmitCommonX64.h | 2 +- CodeGen/src/IrBuilder.cpp | 6 +++ CodeGen/src/IrLoweringA64.cpp | 6 ++- CodeGen/src/IrLoweringX64.cpp | 9 ++-- CodeGen/src/IrTranslation.cpp | 41 ++++++++++---- CodeGen/src/IrTranslation.h | 1 + Common/include/Luau/Bytecode.h | 13 +++-- Compiler/src/BytecodeBuilder.cpp | 22 +++++++- Compiler/src/Compiler.cpp | 16 ++++++ VM/src/lvmexecute.cpp | 57 ++++++++++++++++---- tests/Compiler.test.cpp | 81 ++++++++++++++++++---------- 13 files changed, 203 insertions(+), 60 deletions(-) diff --git a/CodeGen/include/Luau/IrVisitUseDef.h b/CodeGen/include/Luau/IrVisitUseDef.h index 8134e0d5..603f1ec5 100644 --- a/CodeGen/include/Luau/IrVisitUseDef.h +++ b/CodeGen/include/Luau/IrVisitUseDef.h @@ -41,6 +41,11 @@ static void visitVmRegDefsUses(T& visitor, IrFunction& function, const IrInst& i break; // A <- B, C case IrCmd::DO_ARITH: + visitor.maybeUse(inst.b); // Argument can also be a VmConst + visitor.maybeUse(inst.c); // Argument can also be a VmConst + + visitor.def(inst.a); + break; case IrCmd::GET_TABLE: visitor.use(inst.b); visitor.maybeUse(inst.c); // Argument can also be a VmConst diff --git a/CodeGen/src/EmitCommonX64.cpp b/CodeGen/src/EmitCommonX64.cpp index 008dadd5..014f5a46 100644 --- a/CodeGen/src/EmitCommonX64.cpp +++ b/CodeGen/src/EmitCommonX64.cpp @@ -148,12 +148,12 @@ void convertNumberToIndexOrJump(AssemblyBuilderX64& build, RegisterX64 tmp, Regi build.jcc(ConditionX64::NotZero, label); } -void callArithHelper(IrRegAllocX64& regs, AssemblyBuilderX64& build, int ra, int rb, OperandX64 c, TMS tm) +void callArithHelper(IrRegAllocX64& regs, AssemblyBuilderX64& build, int ra, OperandX64 b, OperandX64 c, TMS tm) { IrCallWrapperX64 callWrap(regs, build); callWrap.addArgument(SizeX64::qword, rState); callWrap.addArgument(SizeX64::qword, luauRegAddress(ra)); - callWrap.addArgument(SizeX64::qword, luauRegAddress(rb)); + callWrap.addArgument(SizeX64::qword, b); callWrap.addArgument(SizeX64::qword, c); callWrap.addArgument(SizeX64::dword, tm); callWrap.call(qword[rNativeContext + offsetof(NativeContext, luaV_doarith)]); diff --git a/CodeGen/src/EmitCommonX64.h b/CodeGen/src/EmitCommonX64.h index 3418a09f..bc1f99c9 100644 --- a/CodeGen/src/EmitCommonX64.h +++ b/CodeGen/src/EmitCommonX64.h @@ -200,7 +200,7 @@ ConditionX64 getConditionInt(IrCondition cond); void getTableNodeAtCachedSlot(AssemblyBuilderX64& build, RegisterX64 tmp, RegisterX64 node, RegisterX64 table, int pcpos); void convertNumberToIndexOrJump(AssemblyBuilderX64& build, RegisterX64 tmp, RegisterX64 numd, RegisterX64 numi, Label& label); -void callArithHelper(IrRegAllocX64& regs, AssemblyBuilderX64& build, int ra, int rb, OperandX64 c, TMS tm); +void callArithHelper(IrRegAllocX64& regs, AssemblyBuilderX64& build, int ra, OperandX64 b, OperandX64 c, TMS tm); void callLengthHelper(IrRegAllocX64& regs, AssemblyBuilderX64& build, int ra, int rb); void callGetTable(IrRegAllocX64& regs, AssemblyBuilderX64& build, int rb, OperandX64 c, int ra); void callSetTable(IrRegAllocX64& regs, AssemblyBuilderX64& build, int rb, OperandX64 c, int ra); diff --git a/CodeGen/src/IrBuilder.cpp b/CodeGen/src/IrBuilder.cpp index 56bbf904..765433a4 100644 --- a/CodeGen/src/IrBuilder.cpp +++ b/CodeGen/src/IrBuilder.cpp @@ -381,6 +381,12 @@ void IrBuilder::translateInst(LuauOpcode op, const Instruction* pc, int i) case LOP_POWK: translateInstBinaryK(*this, pc, i, TM_POW); break; + case LOP_SUBRK: + translateInstBinaryRK(*this, pc, i, TM_SUB); + break; + case LOP_DIVRK: + translateInstBinaryRK(*this, pc, i, TM_DIV); + break; case LOP_NOT: translateInstNot(*this, pc); break; diff --git a/CodeGen/src/IrLoweringA64.cpp b/CodeGen/src/IrLoweringA64.cpp index 6a1733a5..5d0be75a 100644 --- a/CodeGen/src/IrLoweringA64.cpp +++ b/CodeGen/src/IrLoweringA64.cpp @@ -1067,7 +1067,11 @@ void IrLoweringA64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next) regs.spill(build, index); build.mov(x0, rState); build.add(x1, rBase, uint16_t(vmRegOp(inst.a) * sizeof(TValue))); - build.add(x2, rBase, uint16_t(vmRegOp(inst.b) * sizeof(TValue))); + + if (inst.b.kind == IrOpKind::VmConst) + emitAddOffset(build, x2, rConstants, vmConstOp(inst.b) * sizeof(TValue)); + else + build.add(x2, rBase, uint16_t(vmRegOp(inst.b) * sizeof(TValue))); if (inst.c.kind == IrOpKind::VmConst) emitAddOffset(build, x3, rConstants, vmConstOp(inst.c) * sizeof(TValue)); diff --git a/CodeGen/src/IrLoweringX64.cpp b/CodeGen/src/IrLoweringX64.cpp index 74a5bfd6..ca06ca15 100644 --- a/CodeGen/src/IrLoweringX64.cpp +++ b/CodeGen/src/IrLoweringX64.cpp @@ -962,11 +962,12 @@ void IrLoweringX64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next) break; } case IrCmd::DO_ARITH: - if (inst.c.kind == IrOpKind::VmReg) - callArithHelper(regs, build, vmRegOp(inst.a), vmRegOp(inst.b), luauRegAddress(vmRegOp(inst.c)), TMS(intOp(inst.d))); - else - callArithHelper(regs, build, vmRegOp(inst.a), vmRegOp(inst.b), luauConstantAddress(vmConstOp(inst.c)), TMS(intOp(inst.d))); + { + OperandX64 opb = inst.b.kind == IrOpKind::VmReg ? luauRegAddress(vmRegOp(inst.b)) : luauConstantAddress(vmConstOp(inst.b)); + OperandX64 opc = inst.c.kind == IrOpKind::VmReg ? luauRegAddress(vmRegOp(inst.c)) : luauConstantAddress(vmConstOp(inst.c)); + callArithHelper(regs, build, vmRegOp(inst.a), opb, opc, TMS(intOp(inst.d))); break; + } case IrCmd::DO_LEN: callLengthHelper(regs, build, vmRegOp(inst.a), vmRegOp(inst.b)); break; diff --git a/CodeGen/src/IrTranslation.cpp b/CodeGen/src/IrTranslation.cpp index 91e87fdb..76d58265 100644 --- a/CodeGen/src/IrTranslation.cpp +++ b/CodeGen/src/IrTranslation.cpp @@ -327,13 +327,16 @@ void translateInstJumpxEqS(IrBuilder& build, const Instruction* pc, int pcpos) build.beginBlock(next); } -static void translateInstBinaryNumeric(IrBuilder& build, int ra, int rb, int rc, IrOp opc, int pcpos, TMS tm) +static void translateInstBinaryNumeric(IrBuilder& build, int ra, int rb, int rc, IrOp opb, IrOp opc, int pcpos, TMS tm) { IrOp fallback = build.block(IrBlockKind::Fallback); // fast-path: number - IrOp tb = build.inst(IrCmd::LOAD_TAG, build.vmReg(rb)); - build.inst(IrCmd::CHECK_TAG, tb, build.constTag(LUA_TNUMBER), fallback); + if (rb != -1) + { + IrOp tb = build.inst(IrCmd::LOAD_TAG, build.vmReg(rb)); + build.inst(IrCmd::CHECK_TAG, tb, build.constTag(LUA_TNUMBER), fallback); + } if (rc != -1 && rc != rb) // TODO: optimization should handle second check, but we'll test it later { @@ -341,11 +344,23 @@ static void translateInstBinaryNumeric(IrBuilder& build, int ra, int rb, int rc, build.inst(IrCmd::CHECK_TAG, tc, build.constTag(LUA_TNUMBER), fallback); } - IrOp vb = build.inst(IrCmd::LOAD_DOUBLE, build.vmReg(rb)); - IrOp vc; - + IrOp vb, vc; IrOp result; + if (opb.kind == IrOpKind::VmConst) + { + LUAU_ASSERT(build.function.proto); + TValue protok = build.function.proto->k[vmConstOp(opb)]; + + LUAU_ASSERT(protok.tt == LUA_TNUMBER); + + vb = build.constDouble(protok.value.n); + } + else + { + vb = build.inst(IrCmd::LOAD_DOUBLE, opb); + } + if (opc.kind == IrOpKind::VmConst) { LUAU_ASSERT(build.function.proto); @@ -409,18 +424,26 @@ static void translateInstBinaryNumeric(IrBuilder& build, int ra, int rb, int rc, FallbackStreamScope scope(build, fallback, next); build.inst(IrCmd::SET_SAVEDPC, build.constUint(pcpos + 1)); - build.inst(IrCmd::DO_ARITH, build.vmReg(ra), build.vmReg(rb), opc, build.constInt(tm)); + build.inst(IrCmd::DO_ARITH, build.vmReg(ra), opb, opc, build.constInt(tm)); build.inst(IrCmd::JUMP, next); } void translateInstBinary(IrBuilder& build, const Instruction* pc, int pcpos, TMS tm) { - translateInstBinaryNumeric(build, LUAU_INSN_A(*pc), LUAU_INSN_B(*pc), LUAU_INSN_C(*pc), build.vmReg(LUAU_INSN_C(*pc)), pcpos, tm); + translateInstBinaryNumeric( + build, LUAU_INSN_A(*pc), LUAU_INSN_B(*pc), LUAU_INSN_C(*pc), build.vmReg(LUAU_INSN_B(*pc)), build.vmReg(LUAU_INSN_C(*pc)), pcpos, tm); } void translateInstBinaryK(IrBuilder& build, const Instruction* pc, int pcpos, TMS tm) { - translateInstBinaryNumeric(build, LUAU_INSN_A(*pc), LUAU_INSN_B(*pc), -1, build.vmConst(LUAU_INSN_C(*pc)), pcpos, tm); + translateInstBinaryNumeric( + build, LUAU_INSN_A(*pc), LUAU_INSN_B(*pc), -1, build.vmReg(LUAU_INSN_B(*pc)), build.vmConst(LUAU_INSN_C(*pc)), pcpos, tm); +} + +void translateInstBinaryRK(IrBuilder& build, const Instruction* pc, int pcpos, TMS tm) +{ + translateInstBinaryNumeric( + build, LUAU_INSN_A(*pc), -1, LUAU_INSN_C(*pc), build.vmConst(LUAU_INSN_B(*pc)), build.vmReg(LUAU_INSN_C(*pc)), pcpos, tm); } void translateInstNot(IrBuilder& build, const Instruction* pc) diff --git a/CodeGen/src/IrTranslation.h b/CodeGen/src/IrTranslation.h index 29c14356..99c3d1c3 100644 --- a/CodeGen/src/IrTranslation.h +++ b/CodeGen/src/IrTranslation.h @@ -35,6 +35,7 @@ void translateInstJumpxEqN(IrBuilder& build, const Instruction* pc, int pcpos); void translateInstJumpxEqS(IrBuilder& build, const Instruction* pc, int pcpos); void translateInstBinary(IrBuilder& build, const Instruction* pc, int pcpos, TMS tm); void translateInstBinaryK(IrBuilder& build, const Instruction* pc, int pcpos, TMS tm); +void translateInstBinaryRK(IrBuilder& build, const Instruction* pc, int pcpos, TMS tm); void translateInstNot(IrBuilder& build, const Instruction* pc); void translateInstMinus(IrBuilder& build, const Instruction* pc, int pcpos); void translateInstLength(IrBuilder& build, const Instruction* pc, int pcpos); diff --git a/Common/include/Luau/Bytecode.h b/Common/include/Luau/Bytecode.h index 36dfabdb..e3c20670 100644 --- a/Common/include/Luau/Bytecode.h +++ b/Common/include/Luau/Bytecode.h @@ -45,7 +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. +// Version 5: Adds SUBRK/DIVRK and vector constants. Currently supported. // Bytecode opcode, part of the instruction header enum LuauOpcode @@ -219,7 +219,7 @@ enum LuauOpcode // ADDK, SUBK, MULK, DIVK, MODK, POWK: compute arithmetic operation between the source register and a constant and put the result into target register // A: target register // B: source register - // C: constant table index (0..255) + // C: constant table index (0..255); must refer to a number LOP_ADDK, LOP_SUBK, LOP_MULK, @@ -348,9 +348,12 @@ enum LuauOpcode // B: source register (for VAL/REF) or upvalue index (for UPVAL/UPREF) LOP_CAPTURE, - // removed in v3 - LOP_DEP_JUMPIFEQK, - LOP_DEP_JUMPIFNOTEQK, + // SUBRK, DIVRK: compute arithmetic operation between the constant and a source register and put the result into target register + // A: target register + // B: source register + // C: constant table index (0..255); must refer to a number + LOP_SUBRK, + LOP_DIVRK, // FASTCALL1: perform a fast call of a built-in function using 1 register argument // A: builtin function id (see LuauBuiltinFunction) diff --git a/Compiler/src/BytecodeBuilder.cpp b/Compiler/src/BytecodeBuilder.cpp index 8dc7b88e..ae376657 100644 --- a/Compiler/src/BytecodeBuilder.cpp +++ b/Compiler/src/BytecodeBuilder.cpp @@ -8,6 +8,7 @@ #include LUAU_FASTFLAG(LuauVectorLiterals) +LUAU_FASTFLAG(LuauCompileRevK) namespace Luau { @@ -1123,7 +1124,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 (FFlag::LuauVectorLiterals ? 5 : LBC_VERSION_TARGET); + return (FFlag::LuauVectorLiterals || FFlag::LuauCompileRevK) ? 5 : LBC_VERSION_TARGET; } uint8_t BytecodeBuilder::getTypeEncodingVersion() @@ -1351,6 +1352,13 @@ void BytecodeBuilder::validateInstructions() const VCONST(LUAU_INSN_C(insn), Number); break; + case LOP_SUBRK: + case LOP_DIVRK: + VREG(LUAU_INSN_A(insn)); + VCONST(LUAU_INSN_B(insn), Number); + VREG(LUAU_INSN_C(insn)); + break; + case LOP_AND: case LOP_OR: VREG(LUAU_INSN_A(insn)); @@ -1973,6 +1981,18 @@ void BytecodeBuilder::dumpInstruction(const uint32_t* code, std::string& result, result.append("]\n"); break; + case LOP_SUBRK: + formatAppend(result, "SUBRK R%d K%d [", LUAU_INSN_A(insn), LUAU_INSN_B(insn)); + dumpConstant(result, LUAU_INSN_B(insn)); + formatAppend(result, "] R%d\n", LUAU_INSN_C(insn)); + break; + + case LOP_DIVRK: + formatAppend(result, "DIVRK R%d K%d [", LUAU_INSN_A(insn), LUAU_INSN_B(insn)); + dumpConstant(result, LUAU_INSN_B(insn)); + formatAppend(result, "] R%d\n", LUAU_INSN_C(insn)); + break; + case LOP_AND: formatAppend(result, "AND R%d R%d R%d\n", LUAU_INSN_A(insn), LUAU_INSN_B(insn), LUAU_INSN_C(insn)); break; diff --git a/Compiler/src/Compiler.cpp b/Compiler/src/Compiler.cpp index 0a5463a2..1a6d6e5c 100644 --- a/Compiler/src/Compiler.cpp +++ b/Compiler/src/Compiler.cpp @@ -29,6 +29,8 @@ LUAU_FASTINTVARIABLE(LuauCompileInlineDepth, 5) LUAU_FASTFLAGVARIABLE(LuauCompileSideEffects, false) LUAU_FASTFLAGVARIABLE(LuauCompileDeadIf, false) +LUAU_FASTFLAGVARIABLE(LuauCompileRevK, false) + namespace Luau { @@ -1516,6 +1518,20 @@ struct Compiler } else { + if (FFlag::LuauCompileRevK && (expr->op == AstExprBinary::Sub || expr->op == AstExprBinary::Div)) + { + int32_t lc = getConstantNumber(expr->left); + + if (lc >= 0 && lc <= 255) + { + uint8_t rr = compileExprAuto(expr->right, rs); + LuauOpcode op = (expr->op == AstExprBinary::Sub) ? LOP_SUBRK : LOP_DIVRK; + + bytecode.emitABC(op, target, uint8_t(lc), uint8_t(rr)); + return; + } + } + uint8_t rl = compileExprAuto(expr->left, rs); uint8_t rr = compileExprAuto(expr->right, rs); diff --git a/VM/src/lvmexecute.cpp b/VM/src/lvmexecute.cpp index 451433ee..c172dfc7 100644 --- a/VM/src/lvmexecute.cpp +++ b/VM/src/lvmexecute.cpp @@ -101,7 +101,7 @@ VM_DISPATCH_OP(LOP_FORGLOOP), VM_DISPATCH_OP(LOP_FORGPREP_INEXT), VM_DISPATCH_OP(LOP_DEP_FORGLOOP_INEXT), VM_DISPATCH_OP(LOP_FORGPREP_NEXT), \ VM_DISPATCH_OP(LOP_NATIVECALL), VM_DISPATCH_OP(LOP_GETVARARGS), VM_DISPATCH_OP(LOP_DUPCLOSURE), VM_DISPATCH_OP(LOP_PREPVARARGS), \ VM_DISPATCH_OP(LOP_LOADKX), VM_DISPATCH_OP(LOP_JUMPX), VM_DISPATCH_OP(LOP_FASTCALL), VM_DISPATCH_OP(LOP_COVERAGE), \ - VM_DISPATCH_OP(LOP_CAPTURE), VM_DISPATCH_OP(LOP_DEP_JUMPIFEQK), VM_DISPATCH_OP(LOP_DEP_JUMPIFNOTEQK), VM_DISPATCH_OP(LOP_FASTCALL1), \ + VM_DISPATCH_OP(LOP_CAPTURE), VM_DISPATCH_OP(LOP_SUBRK), VM_DISPATCH_OP(LOP_DIVRK), VM_DISPATCH_OP(LOP_FASTCALL1), \ VM_DISPATCH_OP(LOP_FASTCALL2), VM_DISPATCH_OP(LOP_FASTCALL2K), VM_DISPATCH_OP(LOP_FORGPREP), VM_DISPATCH_OP(LOP_JUMPXEQKNIL), \ VM_DISPATCH_OP(LOP_JUMPXEQKB), VM_DISPATCH_OP(LOP_JUMPXEQKN), VM_DISPATCH_OP(LOP_JUMPXEQKS), VM_DISPATCH_OP(LOP_IDIV), \ VM_DISPATCH_OP(LOP_IDIVK), @@ -1858,9 +1858,9 @@ reentry: } else if (ttisvector(rb)) { - const float* vb = rb->value.v; - float vc = cast_to(float, nvalue(kv)); - setvvalue(ra, vb[0] / vc, vb[1] / vc, vb[2] / vc, vb[3] / vc); + const float* vb = vvalue(rb); + float nc = cast_to(float, nvalue(kv)); + setvvalue(ra, vb[0] / nc, vb[1] / nc, vb[2] / nc, vb[3] / nc); VM_NEXT(); } else @@ -2697,16 +2697,53 @@ reentry: LUAU_UNREACHABLE(); } - VM_CASE(LOP_DEP_JUMPIFEQK) + VM_CASE(LOP_SUBRK) { - LUAU_ASSERT(!"Unsupported deprecated opcode"); - LUAU_UNREACHABLE(); + Instruction insn = *pc++; + StkId ra = VM_REG(LUAU_INSN_A(insn)); + TValue* kv = VM_KV(LUAU_INSN_B(insn)); + StkId rc = VM_REG(LUAU_INSN_C(insn)); + + // fast-path + if (ttisnumber(rc)) + { + setnvalue(ra, nvalue(kv) - nvalue(rc)); + VM_NEXT(); + } + else + { + // slow-path, may invoke C/Lua via metamethods + VM_PROTECT(luaV_doarith(L, ra, kv, rc, TM_SUB)); + VM_NEXT(); + } } - VM_CASE(LOP_DEP_JUMPIFNOTEQK) + VM_CASE(LOP_DIVRK) { - LUAU_ASSERT(!"Unsupported deprecated opcode"); - LUAU_UNREACHABLE(); + Instruction insn = *pc++; + StkId ra = VM_REG(LUAU_INSN_A(insn)); + TValue* kv = VM_KV(LUAU_INSN_B(insn)); + StkId rc = VM_REG(LUAU_INSN_C(insn)); + + // fast-path + if (LUAU_LIKELY(ttisnumber(rc))) + { + setnvalue(ra, nvalue(kv) / nvalue(rc)); + VM_NEXT(); + } + else if (ttisvector(rc)) + { + float nb = cast_to(float, nvalue(kv)); + const float* vc = vvalue(rc); + setvvalue(ra, nb / vc[0], nb / vc[1], nb / vc[2], nb / vc[3]); + VM_NEXT(); + } + else + { + // slow-path, may invoke C/Lua via metamethods + VM_PROTECT(luaV_doarith(L, ra, kv, rc, TM_DIV)); + VM_NEXT(); + } } VM_CASE(LOP_FASTCALL1) diff --git a/tests/Compiler.test.cpp b/tests/Compiler.test.cpp index c605a364..4abf4a7c 100644 --- a/tests/Compiler.test.cpp +++ b/tests/Compiler.test.cpp @@ -1173,6 +1173,8 @@ RETURN R0 1 TEST_CASE("AndOrChainCodegen") { + ScopedFastFlag sff("LuauCompileRevK", true); + const char* source = R"( return (1 - verticalGradientTurbulence < waterLevel + .015 and Enum.Material.Sand) @@ -1181,23 +1183,22 @@ TEST_CASE("AndOrChainCodegen") )"; CHECK_EQ("\n" + compileFunction0(source), R"( -LOADN R2 1 -GETIMPORT R3 1 [verticalGradientTurbulence] -SUB R1 R2 R3 -GETIMPORT R3 4 [waterLevel] -ADDK R2 R3 K2 [0.014999999999999999] +GETIMPORT R2 2 [verticalGradientTurbulence] +SUBRK R1 K0 [1] R2 +GETIMPORT R3 5 [waterLevel] +ADDK R2 R3 K3 [0.014999999999999999] JUMPIFNOTLT R1 R2 L0 -GETIMPORT R0 8 [Enum.Material.Sand] +GETIMPORT R0 9 [Enum.Material.Sand] JUMPIF R0 L2 -L0: GETIMPORT R1 10 [sandbank] +L0: GETIMPORT R1 11 [sandbank] LOADN R2 0 JUMPIFNOTLT R2 R1 L1 -GETIMPORT R1 10 [sandbank] +GETIMPORT R1 11 [sandbank] LOADN R2 1 JUMPIFNOTLT R1 R2 L1 -GETIMPORT R0 8 [Enum.Material.Sand] +GETIMPORT R0 9 [Enum.Material.Sand] JUMPIF R0 L2 -L1: GETIMPORT R0 12 [Enum.Material.Sandstone] +L1: GETIMPORT R0 13 [Enum.Material.Sandstone] L2: RETURN R0 1 )"); } @@ -2096,6 +2097,8 @@ RETURN R0 0 TEST_CASE("AndOrOptimizations") { + ScopedFastFlag sff("LuauCompileRevK", true); + // the OR/ORK optimization triggers for cutoff since lhs is simple CHECK_EQ("\n" + compileFunction(R"( local function advancedRidgedFilter(value, cutoff) @@ -2108,17 +2111,15 @@ end R"( ORK R2 R1 K0 [0.5] SUB R0 R0 R2 -LOADN R4 1 -LOADN R8 0 -JUMPIFNOTLT R0 R8 L0 -MINUS R7 R0 -JUMPIF R7 L1 -L0: MOVE R7 R0 -L1: MULK R6 R7 K1 [1] -LOADN R8 1 -SUB R7 R8 R2 -DIV R5 R6 R7 -SUB R3 R4 R5 +LOADN R7 0 +JUMPIFNOTLT R0 R7 L0 +MINUS R6 R0 +JUMPIF R6 L1 +L0: MOVE R6 R0 +L1: MULK R5 R6 K1 [1] +SUBRK R6 K1 [1] R2 +DIV R4 R5 R6 +SUBRK R3 K1 [1] R4 RETURN R3 1 )"); @@ -2131,9 +2132,8 @@ end 0), R"( LOADB R2 0 -LOADK R4 K0 [0.5] -MULK R5 R1 K1 [0.40000000000000002] -SUB R3 R4 R5 +MULK R4 R1 K1 [0.40000000000000002] +SUBRK R3 K0 [0.5] R4 JUMPIFNOTLT R3 R0 L1 LOADK R4 K0 [0.5] MULK R5 R1 K1 [0.40000000000000002] @@ -2153,9 +2153,8 @@ end 0), R"( LOADB R2 1 -LOADK R4 K0 [0.5] -MULK R5 R1 K1 [0.40000000000000002] -SUB R3 R4 R5 +MULK R4 R1 K1 [0.40000000000000002] +SUBRK R3 K0 [0.5] R4 JUMPIFLT R0 R3 L1 LOADK R4 K0 [0.5] MULK R5 R1 K1 [0.40000000000000002] @@ -7847,4 +7846,32 @@ RETURN R0 1 )"); } +TEST_CASE("ArithRevK") +{ + ScopedFastFlag sff("LuauCompileRevK", true); + + // - and / have special optimized form for reverse constants; in the future, + and * will likely get compiled to ADDK/MULK + // other operators are not important enough to optimize reverse constant forms for + CHECK_EQ("\n" + compileFunction0(R"( +local x: number = unknown +return 2 + x, 2 - x, 2 * x, 2 / x, 2 % x, 2 // x, 2 ^ x +)"), + R"( +GETIMPORT R0 1 [unknown] +LOADN R2 2 +ADD R1 R2 R0 +SUBRK R2 K2 [2] R0 +LOADN R4 2 +MUL R3 R4 R0 +DIVRK R4 K2 [2] R0 +LOADN R6 2 +MOD R5 R6 R0 +LOADN R7 2 +IDIV R6 R7 R0 +LOADN R8 2 +POW R7 R8 R0 +RETURN R1 7 +)"); +} + TEST_SUITE_END(); From c755875479fe8b0d46473317802f5440bb7d1723 Mon Sep 17 00:00:00 2001 From: Vighnesh-V Date: Fri, 1 Dec 2023 23:46:57 -0800 Subject: [PATCH 14/19] Sync to upstream/release/605 (#1118) - Implemented [Require by String with Relative Paths](https://github.com/luau-lang/rfcs/blob/master/docs/new-require-by-string-semantics.md) RFC - Implemented [Require by String with Aliases](https://github.com/luau-lang/rfcs/blob/master/docs/require-by-string-aliases.md) RFC with support for `paths` and `alias` arrays in .luarc - Added SUBRK and DIVRK bytecode instructions to speed up constant-number and constant/number operations - Added `--vector-lib`, `--vector-ctor` and `--vector-type` options to luau-compile to support code with vectors New Solver - Correctness fixes to subtyping - Improvements to dataflow analysis Native Code Generation - Added bytecode analysis pass to predict type tags used in operations - Fixed rare cases of numerical loops being generated without an interrupt instruction - Restored optimization data propagation into the linear block - Duplicate buffer length checks are optimized away Miscellaneous - Small performance improvements to new non-strict mode - Introduced more scripts for fuzzing Luau and processing the results, including fuzzer build support for CMake Co-authored-by: Alexander McCord Co-authored-by: Andy Friesen Co-authored-by: Aviral Goel Co-authored-by: David Cope Co-authored-by: Lily Brown Co-authored-by: Vighnesh Vijay Co-authored-by: Vyacheslav Egorov --------- Co-authored-by: Aaron Weiss Co-authored-by: Alexander McCord Co-authored-by: Andy Friesen Co-authored-by: Aviral Goel Co-authored-by: David Cope Co-authored-by: Lily Brown Co-authored-by: Vyacheslav Egorov --- Analysis/include/Luau/Constraint.h | 4 +- Analysis/include/Luau/ConstraintGenerator.h | 1 + Analysis/include/Luau/Error.h | 10 +- Analysis/include/Luau/Subtyping.h | 12 +- Analysis/include/Luau/Unifier2.h | 2 +- Analysis/src/ConstraintGenerator.cpp | 54 +- Analysis/src/ConstraintSolver.cpp | 56 +- Analysis/src/Error.cpp | 15 + Analysis/src/IostreamHelpers.cpp | 3 + Analysis/src/NonStrictTypeChecker.cpp | 64 +- Analysis/src/Normalize.cpp | 6 +- Analysis/src/Subtyping.cpp | 202 +++- Analysis/src/ToString.cpp | 2 +- Analysis/src/TypeChecker2.cpp | 2 + Analysis/src/TypePath.cpp | 53 +- Analysis/src/Unifier2.cpp | 19 +- Ast/include/Luau/Ast.h | 12 + Ast/src/Parser.cpp | 47 +- CLI/Compile.cpp | 64 +- CLI/FileUtils.cpp | 161 ++++ CLI/FileUtils.h | 9 + CLI/Repl.cpp | 183 ++-- CLI/Require.cpp | 290 ++++++ CLI/Require.h | 62 ++ CMakeLists.txt | 6 +- CMakePresets.json | 47 + CodeGen/include/Luau/BytecodeAnalysis.h | 21 + CodeGen/include/Luau/CodeGen.h | 15 + CodeGen/include/Luau/IrBuilder.h | 8 +- CodeGen/include/Luau/IrData.h | 69 +- CodeGen/include/Luau/IrDump.h | 1 + CodeGen/src/BytecodeAnalysis.cpp | 884 ++++++++++++++++++ CodeGen/src/CodeGenAssembly.cpp | 34 +- CodeGen/src/CodeGenLower.h | 25 +- CodeGen/src/CodeGenUtils.cpp | 2 +- CodeGen/src/IrBuilder.cpp | 13 +- CodeGen/src/IrDump.cpp | 46 +- CodeGen/src/IrLoweringX64.cpp | 2 +- CodeGen/src/IrTranslateBuiltins.cpp | 31 +- CodeGen/src/IrTranslation.cpp | 184 ++-- CodeGen/src/IrTranslation.h | 2 +- CodeGen/src/OptimizeConstProp.cpp | 126 ++- Compiler/src/BuiltinFolding.cpp | 4 +- Compiler/src/BytecodeBuilder.cpp | 3 +- Compiler/src/Compiler.cpp | 31 +- Compiler/src/ConstantFolding.cpp | 7 +- Config/include/Luau/Config.h | 8 +- Config/src/Config.cpp | 45 + Makefile | 6 +- Sources.cmake | 9 +- VM/src/lvmexecute.cpp | 30 +- VM/src/lvmutils.cpp | 2 +- fuzz/CMakeLists.txt | 105 +++ fuzz/libprotobuf-mutator-patch.patch | 12 + tests/AstJsonEncoder.test.cpp | 6 +- tests/Autocomplete.test.cpp | 6 +- tests/Compiler.test.cpp | 46 +- tests/Conformance.test.cpp | 36 +- tests/ConstraintGeneratorFixture.cpp | 5 +- tests/DataFlowGraph.test.cpp | 4 +- tests/Differ.test.cpp | 54 +- tests/Error.test.cpp | 4 +- tests/Fixture.cpp | 3 +- tests/Fixture.h | 4 +- tests/Frontend.test.cpp | 6 +- tests/IrBuilder.test.cpp | 117 ++- tests/Linter.test.cpp | 4 - tests/Module.test.cpp | 19 +- tests/NonStrictTypeChecker.test.cpp | 258 ++++- tests/Normalize.test.cpp | 13 +- tests/Parser.test.cpp | 47 +- tests/RequireByString.test.cpp | 395 ++++++++ tests/RuntimeLimits.test.cpp | 7 +- tests/ScopedFlags.h | 15 +- tests/Simplify.test.cpp | 4 +- tests/Subtyping.test.cpp | 37 +- tests/Symbol.test.cpp | 4 +- tests/ToDot.test.cpp | 4 +- tests/ToString.test.cpp | 22 +- tests/TxnLog.test.cpp | 8 +- tests/TypeInfer.aliases.test.cpp | 9 +- tests/TypeInfer.annotations.test.cpp | 17 +- tests/TypeInfer.builtins.test.cpp | 3 +- tests/TypeInfer.cfa.test.cpp | 165 +--- tests/TypeInfer.classes.test.cpp | 3 +- tests/TypeInfer.definitions.test.cpp | 4 +- tests/TypeInfer.functions.test.cpp | 10 +- tests/TypeInfer.generics.test.cpp | 5 +- tests/TypeInfer.intersectionTypes.test.cpp | 14 +- tests/TypeInfer.loops.test.cpp | 4 +- tests/TypeInfer.modules.test.cpp | 3 +- tests/TypeInfer.oop.test.cpp | 3 +- tests/TypeInfer.provisional.test.cpp | 33 +- tests/TypeInfer.refinements.test.cpp | 6 +- tests/TypeInfer.rwprops.test.cpp | 3 +- tests/TypeInfer.singletons.test.cpp | 2 +- tests/TypeInfer.tables.test.cpp | 69 +- tests/TypeInfer.test.cpp | 29 +- tests/TypeInfer.tryUnify.test.cpp | 24 +- ...Packs.cpp => TypeInfer.typePacks.test.cpp} | 7 +- tests/TypeInfer.typestates.test.cpp | 2 +- tests/TypeInfer.unionTypes.test.cpp | 5 +- tests/TypeInfer.unknownnever.test.cpp | 4 +- tests/TypePath.test.cpp | 31 +- tests/Unifier2.test.cpp | 4 +- tests/VisitType.test.cpp | 7 +- tests/conformance/interrupt.lua | 8 + tests/require/with_config/.luaurc | 7 + .../GlobalLuauLibraries/global_library.luau | 1 + .../ProjectLuauLibraries/library.luau | 1 + tests/require/with_config/src/.luaurc | 6 + .../with_config/src/alias_requirer.luau | 1 + tests/require/with_config/src/dependency.luau | 1 + .../with_config/src/fail_requirer.luau | 2 + .../src/global_library_requirer.luau | 2 + .../with_config/src/other_dependency.luau | 1 + .../src/parent_alias_requirer.luau | 1 + tests/require/with_config/src/requirer.luau | 2 + tests/require/without_config/dependency.luau | 1 + tests/require/without_config/lua/init.lua | 1 + .../require/without_config/lua_dependency.lua | 1 + tests/require/without_config/luau/init.lua | 1 + tests/require/without_config/luau/init.luau | 1 + tests/require/without_config/module.luau | 3 + tools/faillist.txt | 26 - tools/fuzz/fuzzer-postprocess.py | 69 +- tools/fuzz/fuzzfilter.py | 54 +- tools/fuzz/templates/index.html | 2 + 128 files changed, 3907 insertions(+), 990 deletions(-) create mode 100644 CLI/Require.cpp create mode 100644 CLI/Require.h create mode 100644 CMakePresets.json create mode 100644 CodeGen/include/Luau/BytecodeAnalysis.h create mode 100644 CodeGen/src/BytecodeAnalysis.cpp create mode 100644 fuzz/CMakeLists.txt create mode 100644 fuzz/libprotobuf-mutator-patch.patch create mode 100644 tests/RequireByString.test.cpp rename tests/{TypeInfer.typePacks.cpp => TypeInfer.typePacks.test.cpp} (99%) create mode 100644 tests/require/with_config/.luaurc create mode 100644 tests/require/with_config/GlobalLuauLibraries/global_library.luau create mode 100644 tests/require/with_config/ProjectLuauLibraries/library.luau create mode 100644 tests/require/with_config/src/.luaurc create mode 100644 tests/require/with_config/src/alias_requirer.luau create mode 100644 tests/require/with_config/src/dependency.luau create mode 100644 tests/require/with_config/src/fail_requirer.luau create mode 100644 tests/require/with_config/src/global_library_requirer.luau create mode 100644 tests/require/with_config/src/other_dependency.luau create mode 100644 tests/require/with_config/src/parent_alias_requirer.luau create mode 100644 tests/require/with_config/src/requirer.luau create mode 100644 tests/require/without_config/dependency.luau create mode 100644 tests/require/without_config/lua/init.lua create mode 100644 tests/require/without_config/lua_dependency.lua create mode 100644 tests/require/without_config/luau/init.lua create mode 100644 tests/require/without_config/luau/init.luau create mode 100644 tests/require/without_config/module.luau diff --git a/Analysis/include/Luau/Constraint.h b/Analysis/include/Luau/Constraint.h index a026fdae..bba3fced 100644 --- a/Analysis/include/Luau/Constraint.h +++ b/Analysis/include/Luau/Constraint.h @@ -49,8 +49,8 @@ struct InstantiationConstraint TypeId superType; }; -// iteratee is iterable -// iterators is the iteration types. +// variables ~ iterate iterator +// Unpack the iterator, figure out what types it iterates over, and bind those types to variables. struct IterableConstraint { TypePackId iterator; diff --git a/Analysis/include/Luau/ConstraintGenerator.h b/Analysis/include/Luau/ConstraintGenerator.h index 69b0fd20..a3e1092f 100644 --- a/Analysis/include/Luau/ConstraintGenerator.h +++ b/Analysis/include/Luau/ConstraintGenerator.h @@ -225,6 +225,7 @@ private: Inference check(const ScopePtr& scope, AstExprConstantBool* bool_, std::optional expectedType, bool forceSingleton); Inference check(const ScopePtr& scope, AstExprLocal* local); Inference check(const ScopePtr& scope, AstExprGlobal* global); + Inference checkIndexName(const ScopePtr& scope, const RefinementKey* key, AstExpr* indexee, std::string index); Inference check(const ScopePtr& scope, AstExprIndexName* indexName); Inference check(const ScopePtr& scope, AstExprIndexExpr* indexExpr); Inference check(const ScopePtr& scope, AstExprFunction* func, std::optional expectedType, bool generalize); diff --git a/Analysis/include/Luau/Error.h b/Analysis/include/Luau/Error.h index ddbf6dcb..06ea9601 100644 --- a/Analysis/include/Luau/Error.h +++ b/Analysis/include/Luau/Error.h @@ -372,13 +372,21 @@ struct CheckedFunctionCallError bool operator==(const CheckedFunctionCallError& rhs) const; }; +struct NonStrictFunctionDefinitionError +{ + std::string functionName; + std::string argument; + TypeId argumentType; + bool operator==(const NonStrictFunctionDefinitionError& rhs) const; +}; + using TypeErrorData = Variant; + UninhabitedTypePackFamily, WhereClauseNeeded, PackWhereClauseNeeded, CheckedFunctionCallError, NonStrictFunctionDefinitionError>; struct TypeErrorSummary { diff --git a/Analysis/include/Luau/Subtyping.h b/Analysis/include/Luau/Subtyping.h index 926ffc9c..d2619fe2 100644 --- a/Analysis/include/Luau/Subtyping.h +++ b/Analysis/include/Luau/Subtyping.h @@ -32,12 +32,17 @@ enum class SubtypingVariance // Used for an empty key. Should never appear in actual code. Invalid, Covariant, + // This is used to identify cases where we have a covariant + a + // contravariant reason and we need to merge them. + Contravariant, Invariant, }; struct SubtypingReasoning { + // The path, relative to the _root subtype_, where subtyping failed. Path subPath; + // The path, relative to the _root supertype_, where subtyping failed. Path superPath; SubtypingVariance variance = SubtypingVariance::Covariant; @@ -49,6 +54,9 @@ struct SubtypingReasoningHash size_t operator()(const SubtypingReasoning& r) const; }; +using SubtypingReasonings = DenseHashSet; +static const SubtypingReasoning kEmptyReasoning = SubtypingReasoning{TypePath::kEmpty, TypePath::kEmpty, SubtypingVariance::Invalid}; + struct SubtypingResult { bool isSubtype = false; @@ -58,8 +66,7 @@ struct SubtypingResult /// The reason for isSubtype to be false. May not be present even if /// isSubtype is false, depending on the input types. - DenseHashSet reasoning{ - SubtypingReasoning{TypePath::kEmpty, TypePath::kEmpty, SubtypingVariance::Invalid}}; + SubtypingReasonings reasoning{kEmptyReasoning}; SubtypingResult& andAlso(const SubtypingResult& other); SubtypingResult& orElse(const SubtypingResult& other); @@ -69,7 +76,6 @@ struct SubtypingResult SubtypingResult& withBothPath(TypePath::Path path); SubtypingResult& withSubPath(TypePath::Path path); SubtypingResult& withSuperPath(TypePath::Path path); - SubtypingResult& withVariance(SubtypingVariance variance); // Only negates the `isSubtype`. static SubtypingResult negate(const SubtypingResult& result); diff --git a/Analysis/include/Luau/Unifier2.h b/Analysis/include/Luau/Unifier2.h index 49a275d5..4930df6f 100644 --- a/Analysis/include/Luau/Unifier2.h +++ b/Analysis/include/Luau/Unifier2.h @@ -61,7 +61,7 @@ struct Unifier2 bool unify(TypeId subTy, const UnionType* superUnion); bool unify(const IntersectionType* subIntersection, TypeId superTy); bool unify(TypeId subTy, const IntersectionType* superIntersection); - bool unify(const TableType* subTable, const TableType* superTable); + bool unify(TableType* subTable, const TableType* superTable); bool unify(const MetatableType* subMetatable, const MetatableType* superMetatable); // TODO think about this one carefully. We don't do unions or intersections of type packs diff --git a/Analysis/src/ConstraintGenerator.cpp b/Analysis/src/ConstraintGenerator.cpp index 12e4e7da..cdfe9a7f 100644 --- a/Analysis/src/ConstraintGenerator.cpp +++ b/Analysis/src/ConstraintGenerator.cpp @@ -750,17 +750,18 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatForIn* forI for (AstLocal* var : forIn->vars) { - TypeId ty = nullptr; - if (var->annotation) - ty = resolveType(loopScope, var->annotation, /*inTypeArguments*/ false); - else - ty = freshType(loopScope); - - loopScope->bindings[var] = Binding{ty, var->location}; - TypeId assignee = arena->addType(BlockedType{}); variableTypes.push_back(assignee); + if (var->annotation) + { + TypeId annotationTy = resolveType(loopScope, var->annotation, /*inTypeArguments*/ false); + loopScope->bindings[var] = Binding{annotationTy, var->location}; + addConstraint(scope, var->location, SubtypeConstraint{assignee, annotationTy}); + } + else + loopScope->bindings[var] = Binding{assignee, var->location}; + DefId def = dfg->getDef(var); loopScope->lvalueTypes[def] = assignee; } @@ -1439,9 +1440,6 @@ InferencePack ConstraintGenerator::checkPack(const ScopePtr& scope, AstExprCall* module->astOriginalCallTypes[call->func] = fnType; module->astOriginalCallTypes[call] = fnType; - TypeId instantiatedFnType = arena->addType(BlockedType{}); - addConstraint(scope, call->location, InstantiationConstraint{instantiatedFnType, fnType}); - Checkpoint argBeginCheckpoint = checkpoint(this); std::vector args; @@ -1740,12 +1738,11 @@ Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprGlobal* globa } } -Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprIndexName* indexName) +Inference ConstraintGenerator::checkIndexName(const ScopePtr& scope, const RefinementKey* key, AstExpr* indexee, std::string index) { - TypeId obj = check(scope, indexName->expr).ty; + TypeId obj = check(scope, indexee).ty; TypeId result = arena->addType(BlockedType{}); - const RefinementKey* key = dfg->getRefinementKey(indexName); if (key) { if (auto ty = lookup(scope.get(), key->def)) @@ -1754,7 +1751,7 @@ Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprIndexName* in scope->rvalueRefinements[key->def] = result; } - addConstraint(scope, indexName->expr->location, HasPropConstraint{result, obj, indexName->index.value}); + addConstraint(scope, indexee->location, HasPropConstraint{result, obj, std::move(index)}); if (key) return Inference{result, refinementArena.proposition(key, builtinTypes->truthyType)}; @@ -1762,10 +1759,23 @@ Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprIndexName* in return Inference{result}; } +Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprIndexName* indexName) +{ + const RefinementKey* key = dfg->getRefinementKey(indexName); + return checkIndexName(scope, key, indexName->expr, indexName->index.value); +} + Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprIndexExpr* indexExpr) { + if (auto constantString = indexExpr->index->as()) + { + const RefinementKey* key = dfg->getRefinementKey(indexExpr); + return checkIndexName(scope, key, indexExpr->expr, constantString->value.data); + } + TypeId obj = check(scope, indexExpr->expr).ty; TypeId indexType = check(scope, indexExpr->index).ty; + TypeId result = freshType(scope); const RefinementKey* key = dfg->getRefinementKey(indexExpr); @@ -3079,15 +3089,23 @@ struct GlobalPrepopulator : AstVisitor { } + bool visit(AstExprGlobal* global) override + { + if (auto ty = globalScope->lookup(global->name)) + { + DefId def = dfg->getDef(global); + globalScope->lvalueTypes[def] = *ty; + } + + return true; + } + bool visit(AstStatFunction* function) override { if (AstExprGlobal* g = function->name->as()) { TypeId bt = arena->addType(BlockedType{}); globalScope->bindings[g->name] = Binding{bt}; - - DefId def = dfg->getDef(function->name); - globalScope->lvalueTypes[def] = bt; } return true; diff --git a/Analysis/src/ConstraintSolver.cpp b/Analysis/src/ConstraintSolver.cpp index de2f566d..fa0f767b 100644 --- a/Analysis/src/ConstraintSolver.cpp +++ b/Analysis/src/ConstraintSolver.cpp @@ -1263,9 +1263,6 @@ bool ConstraintSolver::tryDispatch(const SetPropConstraint& c, NotNull(subjectType)) - return block(subjectType, constraint); - std::optional existingPropType = subjectType; for (const std::string& segment : c.path) { @@ -1301,25 +1298,13 @@ bool ConstraintSolver::tryDispatch(const SetPropConstraint& c, NotNull(subjectType)) { - TypeId ty = freshType(arena, builtinTypes, constraint->scope); - - // Mint a chain of free tables per c.path - for (auto it = rbegin(c.path); it != rend(c.path); ++it) - { - TableType t{TableState::Free, TypeLevel{}, constraint->scope}; - t.props[*it] = {ty}; - - ty = arena->addType(std::move(t)); - } - - LUAU_ASSERT(ty); - - bind(subjectType, ty); - if (follow(c.resultType) != follow(ty)) - bind(c.resultType, ty); - unblock(subjectType, constraint->location); - unblock(c.resultType, constraint->location); - return true; + /* + * This should never occur because lookupTableProp() will add bounds to + * any free types it encounters. There will always be an + * existingPropType if the subject is free. + */ + LUAU_ASSERT(false); + return false; } else if (auto ttv = getMutable(subjectType)) { @@ -1328,7 +1313,7 @@ bool ConstraintSolver::tryDispatch(const SetPropConstraint& c, NotNullpersistent); ttv->props[c.path[0]] = Property{c.propType}; - bind(c.resultType, c.subjectType); + bind(c.resultType, subjectType); unblock(c.resultType, constraint->location); return true; } @@ -1337,26 +1322,12 @@ bool ConstraintSolver::tryDispatch(const SetPropConstraint& c, NotNullpersistent); updateTheTableType(builtinTypes, NotNull{arena}, subjectType, c.path, c.propType); - bind(c.resultType, c.subjectType); - unblock(subjectType, constraint->location); - unblock(c.resultType, constraint->location); - return true; - } - else - { - bind(c.resultType, subjectType); - unblock(c.resultType, constraint->location); - return true; } } - else - { - // Other kinds of types don't change shape when properties are assigned - // to them. (if they allow properties at all!) - bind(c.resultType, subjectType); - unblock(c.resultType, constraint->location); - return true; - } + + bind(c.resultType, subjectType); + unblock(c.resultType, constraint->location); + return true; } bool ConstraintSolver::tryDispatch(const SetIndexerConstraint& c, NotNull constraint, bool force) @@ -1908,6 +1879,7 @@ bool ConstraintSolver::tryDispatchIterableFunction( TypeId retIndex; if (isNil(firstIndexTy) || isOptional(firstIndexTy)) { + // FIXME freshType is suspect here firstIndex = arena->addType(UnionType{{freshType(arena, builtinTypes, constraint->scope), builtinTypes->nilType}}); retIndex = firstIndex; } @@ -1949,7 +1921,7 @@ bool ConstraintSolver::tryDispatchIterableFunction( modifiedNextRetHead.push_back(*it); TypePackId modifiedNextRetPack = arena->addTypePack(std::move(modifiedNextRetHead), it.tail()); - auto psc = pushConstraint(constraint->scope, constraint->location, PackSubtypeConstraint{c.variables, modifiedNextRetPack}); + auto psc = pushConstraint(constraint->scope, constraint->location, UnpackConstraint{c.variables, modifiedNextRetPack}); inheritBlocks(constraint, psc); return true; diff --git a/Analysis/src/Error.cpp b/Analysis/src/Error.cpp index 3be63f02..5ec2d52b 100644 --- a/Analysis/src/Error.cpp +++ b/Analysis/src/Error.cpp @@ -533,6 +533,12 @@ struct ErrorConverter return "Function '" + e.checkedFunctionName + "' expects '" + toString(e.expected) + "' at argument #" + std::to_string(e.argumentIndex) + ", but got '" + Luau::toString(e.passed) + "'"; } + + std::string operator()(const NonStrictFunctionDefinitionError& e) const + { + return "Argument " + e.argument + " with type '" + toString(e.argumentType) + "' in function '" + e.functionName + + "' is used in a way that will run time error"; + } }; struct InvalidNameChecker @@ -861,6 +867,11 @@ bool CheckedFunctionCallError::operator==(const CheckedFunctionCallError& rhs) c argumentIndex == rhs.argumentIndex; } +bool NonStrictFunctionDefinitionError::operator==(const NonStrictFunctionDefinitionError& rhs) const +{ + return functionName == rhs.functionName && argument == rhs.argument && argumentType == rhs.argumentType; +} + std::string toString(const TypeError& error) { return toString(error, TypeErrorToStringOptions{}); @@ -1032,6 +1043,10 @@ void copyError(T& e, TypeArena& destArena, CloneState& cloneState) e.expected = clone(e.expected); e.passed = clone(e.passed); } + else if constexpr (std::is_same_v) + { + e.argumentType = clone(e.argumentType); + } else static_assert(always_false_v, "Non-exhaustive type switch"); } diff --git a/Analysis/src/IostreamHelpers.cpp b/Analysis/src/IostreamHelpers.cpp index 7a58a244..85a03b48 100644 --- a/Analysis/src/IostreamHelpers.cpp +++ b/Analysis/src/IostreamHelpers.cpp @@ -204,6 +204,9 @@ static void errorToString(std::ostream& stream, const T& err) else if constexpr (std::is_same_v) stream << "CheckedFunctionCallError { expected = '" << toString(err.expected) << "', passed = '" << toString(err.passed) << "', checkedFunctionName = " << err.checkedFunctionName << ", argumentIndex = " << std::to_string(err.argumentIndex) << " }"; + else if constexpr (std::is_same_v) + stream << "NonStrictFunctionDefinitionError { functionName = '" + err.functionName + "', argument = '" + err.argument + + "', argumentType = '" + toString(err.argumentType) + "' }"; else static_assert(always_false_v, "Non-exhaustive type switch"); } diff --git a/Analysis/src/NonStrictTypeChecker.cpp b/Analysis/src/NonStrictTypeChecker.cpp index 451fa8f6..2372a9a7 100644 --- a/Analysis/src/NonStrictTypeChecker.cpp +++ b/Analysis/src/NonStrictTypeChecker.cpp @@ -14,6 +14,7 @@ #include "Luau/Def.h" #include +#include namespace Luau { @@ -105,9 +106,10 @@ struct NonStrictContext return conj; } - void removeFromContext(const std::vector& defs) + // Returns true if the removal was successful + bool remove(const DefId& def) { - // TODO: unimplemented + return context.erase(def.get()) == 1; } std::optional find(const DefId& def) const @@ -138,6 +140,7 @@ struct NonStrictTypeChecker NotNull dfg; DenseHashSet noTypeFamilyErrors{nullptr}; std::vector> stack; + DenseHashMap cachedNegations{nullptr}; const NotNull limits; @@ -271,8 +274,22 @@ struct NonStrictTypeChecker { auto StackPusher = pushStack(block); NonStrictContext ctx; - for (AstStat* statement : block->body) - ctx = NonStrictContext::disjunction(builtinTypes, NotNull{&arena}, ctx, visit(statement)); + + + for (auto it = block->body.rbegin(); it != block->body.rend(); it++) + { + AstStat* stat = *it; + if (AstStatLocal* local = stat->as()) + { + // Iterating in reverse order + // local x ; B generates the context of B without x + visit(local); + for (auto local : local->vars) + ctx.remove(dfg->getDef(local)); + } + else + ctx = NonStrictContext::disjunction(builtinTypes, NotNull{&arena}, visit(stat), ctx); + } return ctx; } @@ -505,9 +522,7 @@ struct NonStrictTypeChecker AstExpr* arg = call->args.data[i]; TypeId expectedArgType = argTypes[i]; DefId def = dfg->getDef(arg); - // TODO: Cache negations created here!!! - // See Jira Ticket: https://roblox.atlassian.net/browse/CLI-87539 - TypeId runTimeErrorTy = arena.addType(NegationType{expectedArgType}); + TypeId runTimeErrorTy = getOrCreateNegation(expectedArgType); fresh.context[def.get()] = runTimeErrorTy; } @@ -537,8 +552,16 @@ struct NonStrictTypeChecker NonStrictContext visit(AstExprFunction* exprFn) { + // TODO: should a function being used as an expression generate a context without the arguments? auto pusher = pushStack(exprFn); - return visit(exprFn->body); + NonStrictContext remainder = visit(exprFn->body); + for (AstLocal* local : exprFn->args) + { + if (std::optional ty = willRunTimeErrorFunctionDefinition(local, remainder)) + reportError(NonStrictFunctionDefinitionError{exprFn->debugname.value, local->name.value, *ty}, local->location); + remainder.remove(dfg->getDef(local)); + } + return remainder; } NonStrictContext visit(AstExprTable* table) @@ -603,6 +626,31 @@ struct NonStrictTypeChecker return {}; } + + std::optional willRunTimeErrorFunctionDefinition(AstLocal* fragment, const NonStrictContext& context) + { + DefId def = dfg->getDef(fragment); + if (std::optional contextTy = context.find(def)) + { + SubtypingResult r1 = subtyping.isSubtype(builtinTypes->unknownType, *contextTy); + SubtypingResult r2 = subtyping.isSubtype(*contextTy, builtinTypes->unknownType); + if (r1.normalizationTooComplex || r2.normalizationTooComplex) + reportError(NormalizationTooComplex{}, fragment->location); + bool isUnknown = r1.isSubtype && r2.isSubtype; + if (isUnknown) + return {builtinTypes->unknownType}; + } + return {}; + } + +private: + TypeId getOrCreateNegation(TypeId baseType) + { + TypeId& cachedResult = cachedNegations[baseType]; + if (!cachedResult) + cachedResult = arena.addType(NegationType{baseType}); + return cachedResult; + }; }; void checkNonStrict(NotNull builtinTypes, NotNull ice, NotNull unifierState, diff --git a/Analysis/src/Normalize.cpp b/Analysis/src/Normalize.cpp index 3c961047..30ab7895 100644 --- a/Analysis/src/Normalize.cpp +++ b/Analysis/src/Normalize.cpp @@ -2772,7 +2772,7 @@ bool Normalizer::intersectNormalWithTy(NormalizedType& here, TypeId there, Set(there) || get(there) || get(there) || get(there) || - get(there)) + get(there) || get(there)) { NormalizedType thereNorm{builtinTypes}; NormalizedType topNorm{builtinTypes}; @@ -2780,6 +2780,10 @@ bool Normalizer::intersectNormalWithTy(NormalizedType& here, TypeId there, Set(std::move(topNorm))); return intersectNormals(here, thereNorm); } + else if (auto lt = get(there)) + { + return intersectNormalWithTy(here, lt->domain, seenSetTypes); + } NormalizedTyvars tyvars = std::move(here.tyvars); diff --git a/Analysis/src/Subtyping.cpp b/Analysis/src/Subtyping.cpp index 7e7c8cd6..49db9cd3 100644 --- a/Analysis/src/Subtyping.cpp +++ b/Analysis/src/Subtyping.cpp @@ -16,6 +16,8 @@ #include +LUAU_FASTFLAGVARIABLE(DebugLuauSubtypingCheckPathValidity, false); + namespace Luau { @@ -55,16 +57,69 @@ size_t SubtypingReasoningHash::operator()(const SubtypingReasoning& r) const return TypePath::PathHash()(r.subPath) ^ (TypePath::PathHash()(r.superPath) << 1) ^ (static_cast(r.variance) << 1); } +template +static void assertReasoningValid(TID subTy, TID superTy, const SubtypingResult& result, NotNull builtinTypes) +{ + if (!FFlag::DebugLuauSubtypingCheckPathValidity) + return; + + for (const SubtypingReasoning& reasoning : result.reasoning) + { + LUAU_ASSERT(traverse(subTy, reasoning.subPath, builtinTypes)); + LUAU_ASSERT(traverse(superTy, reasoning.superPath, builtinTypes)); + } +} + +template<> +void assertReasoningValid(TableIndexer subIdx, TableIndexer superIdx, const SubtypingResult& result, NotNull builtinTypes) +{ + // Empty method to satisfy the compiler. +} + +static SubtypingReasonings mergeReasonings(const SubtypingReasonings& a, const SubtypingReasonings& b) +{ + SubtypingReasonings result{kEmptyReasoning}; + + for (const SubtypingReasoning& r : a) + { + if (r.variance == SubtypingVariance::Invariant) + result.insert(r); + else if (r.variance == SubtypingVariance::Covariant || r.variance == SubtypingVariance::Contravariant) + { + SubtypingReasoning inverseReasoning = SubtypingReasoning{ + r.subPath, r.superPath, r.variance == SubtypingVariance::Covariant ? SubtypingVariance::Contravariant : SubtypingVariance::Covariant}; + if (b.contains(inverseReasoning)) + result.insert(SubtypingReasoning{r.subPath, r.superPath, SubtypingVariance::Invariant}); + else + result.insert(r); + } + } + + for (const SubtypingReasoning& r : b) + { + if (r.variance == SubtypingVariance::Invariant) + result.insert(r); + else if (r.variance == SubtypingVariance::Covariant || r.variance == SubtypingVariance::Contravariant) + { + SubtypingReasoning inverseReasoning = SubtypingReasoning{ + r.subPath, r.superPath, r.variance == SubtypingVariance::Covariant ? SubtypingVariance::Contravariant : SubtypingVariance::Covariant}; + if (a.contains(inverseReasoning)) + result.insert(SubtypingReasoning{r.subPath, r.superPath, SubtypingVariance::Invariant}); + else + result.insert(r); + } + } + + return result; +} + SubtypingResult& SubtypingResult::andAlso(const SubtypingResult& other) { // If 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); - } + reasoning = mergeReasonings(reasoning, other.reasoning); isSubtype &= other.isSubtype; // `|=` is intentional here, we want to preserve error related flags. @@ -86,10 +141,7 @@ SubtypingResult& SubtypingResult::orElse(const SubtypingResult& other) if (other.isSubtype) reasoning.clear(); else - { - for (const SubtypingReasoning& r : other.reasoning) - reasoning.insert(r); - } + reasoning = mergeReasonings(reasoning, other.reasoning); } isSubtype |= other.isSubtype; @@ -162,19 +214,6 @@ SubtypingResult& SubtypingResult::withSuperPath(TypePath::Path path) return *this; } -SubtypingResult& SubtypingResult::withVariance(SubtypingVariance variance) -{ - if (reasoning.empty()) - reasoning.insert(SubtypingReasoning{TypePath::kEmpty, TypePath::kEmpty, variance}); - else - { - for (auto& r : reasoning) - r.variance = variance; - } - - return *this; -} - SubtypingResult SubtypingResult::negate(const SubtypingResult& result) { return SubtypingResult{ @@ -245,7 +284,10 @@ SubtypingResult Subtyping::isSubtype(TypeId subTy, TypeId superTy) result.isSubtype = false; } - result.andAlso(isCovariantWith(env, lowerBound, upperBound)); + SubtypingResult boundsResult = isCovariantWith(env, lowerBound, upperBound); + boundsResult.reasoning.clear(); + + result.andAlso(boundsResult); } /* TODO: We presently don't store subtype test results in the persistent @@ -370,7 +412,10 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypeId sub { SubtypingResult semantic = isCovariantWith(env, normalizer->normalize(subTy), normalizer->normalize(superTy)); if (semantic.isSubtype) + { + semantic.reasoning.clear(); result = semantic; + } } } else if (auto superIntersection = get(superTy)) @@ -382,7 +427,12 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypeId sub { SubtypingResult semantic = isCovariantWith(env, normalizer->normalize(subTy), normalizer->normalize(superTy)); if (semantic.isSubtype) + { + // Clear the semantic reasoning, as any reasonings within + // potentially contain invalid paths. + semantic.reasoning.clear(); result = semantic; + } } } else if (get(superTy)) @@ -411,9 +461,31 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypeId sub else if (auto p = get2(subTy, superTy)) result = isCovariantWith(env, p.first->ty, p.second->ty).withBothComponent(TypePath::TypeField::Negated); else if (auto subNegation = get(subTy)) + { result = isCovariantWith(env, subNegation, superTy); + if (!result.isSubtype && !result.isErrorSuppressing && !result.normalizationTooComplex) + { + SubtypingResult semantic = isCovariantWith(env, normalizer->normalize(subTy), normalizer->normalize(superTy)); + if (semantic.isSubtype) + { + semantic.reasoning.clear(); + result = semantic; + } + } + } else if (auto superNegation = get(superTy)) + { result = isCovariantWith(env, subTy, superNegation); + if (!result.isSubtype && !result.isErrorSuppressing && !result.normalizationTooComplex) + { + SubtypingResult semantic = isCovariantWith(env, normalizer->normalize(subTy), normalizer->normalize(superTy)); + if (semantic.isSubtype) + { + semantic.reasoning.clear(); + result = semantic; + } + } + } else if (auto subGeneric = get(subTy); subGeneric && variance == Variance::Covariant) { bool ok = bindGeneric(env, subTy, superTy); @@ -449,6 +521,8 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypeId sub else if (auto p = get2(subTy, superTy)) result = isCovariantWith(env, p); + assertReasoningValid(subTy, superTy, result, builtinTypes); + return cache(env, result, subTy, superTy); } @@ -536,7 +610,7 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypePackId for (size_t i = headSize; i < subHead.size(); ++i) results.push_back(isCovariantWith(env, subHead[i], vt->ty) .withSubComponent(TypePath::Index{i}) - .withSuperComponent(TypePath::TypeField::Variadic)); + .withSuperPath(TypePath::PathBuilder().tail().variadic().build())); } else if (auto gt = get(*superTail)) { @@ -664,19 +738,38 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypePackId iceReporter->ice("Subtyping test encountered the unexpected type pack: " + toString(*superTail)); } - return SubtypingResult::all(results); + SubtypingResult result = SubtypingResult::all(results); + assertReasoningValid(subTp, superTp, result, builtinTypes); + + return result; } template SubtypingResult Subtyping::isContravariantWith(SubtypingEnvironment& env, SubTy&& subTy, SuperTy&& superTy) { SubtypingResult result = isCovariantWith(env, superTy, subTy); - // If we don't swap the paths here, we will end up producing an invalid path - // whenever we involve contravariance. We'll end up appending path - // components that should belong to the supertype to the subtype, and vice - // versa. - for (auto& reasoning : result.reasoning) - std::swap(reasoning.subPath, reasoning.superPath); + if (result.reasoning.empty()) + result.reasoning.insert(SubtypingReasoning{TypePath::kEmpty, TypePath::kEmpty, SubtypingVariance::Contravariant}); + else + { + // If we don't swap the paths here, we will end up producing an invalid path + // whenever we involve contravariance. We'll end up appending path + // components that should belong to the supertype to the subtype, and vice + // versa. + for (auto& reasoning : result.reasoning) + { + std::swap(reasoning.subPath, reasoning.superPath); + + // Also swap covariant/contravariant, since those are also the other way + // around. + if (reasoning.variance == SubtypingVariance::Covariant) + reasoning.variance = SubtypingVariance::Contravariant; + else if (reasoning.variance == SubtypingVariance::Contravariant) + reasoning.variance = SubtypingVariance::Covariant; + } + } + + assertReasoningValid(subTy, superTy, result, builtinTypes); return result; } @@ -684,7 +777,17 @@ SubtypingResult Subtyping::isContravariantWith(SubtypingEnvironment& env, SubTy& template SubtypingResult Subtyping::isInvariantWith(SubtypingEnvironment& env, SubTy&& subTy, SuperTy&& superTy) { - return isCovariantWith(env, subTy, superTy).andAlso(isContravariantWith(env, subTy, superTy)).withVariance(SubtypingVariance::Invariant); + SubtypingResult result = isCovariantWith(env, subTy, superTy).andAlso(isContravariantWith(env, subTy, superTy)); + if (result.reasoning.empty()) + result.reasoning.insert(SubtypingReasoning{TypePath::kEmpty, TypePath::kEmpty, SubtypingVariance::Invariant}); + else + { + for (auto& reasoning : result.reasoning) + reasoning.variance = SubtypingVariance::Invariant; + } + + assertReasoningValid(subTy, superTy, result, builtinTypes); + return result; } template @@ -696,13 +799,13 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const TryP template SubtypingResult Subtyping::isContravariantWith(SubtypingEnvironment& env, const TryPair& pair) { - return isCovariantWith(env, pair.second, pair.first); + return isContravariantWith(env, pair.first, pair.second); } template SubtypingResult Subtyping::isInvariantWith(SubtypingEnvironment& env, const TryPair& pair) { - return isCovariantWith(env, pair).andAlso(isContravariantWith(pair)).withVariance(SubtypingVariance::Invariant); + return isInvariantWith(env, pair.first, pair.second); } /* @@ -788,17 +891,17 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const Nega if (is(negatedTy)) { // ยฌnever ~ unknown - result = isCovariantWith(env, builtinTypes->unknownType, superTy); + result = isCovariantWith(env, builtinTypes->unknownType, superTy).withSubComponent(TypePath::TypeField::Negated); } else if (is(negatedTy)) { // ยฌunknown ~ never - result = isCovariantWith(env, builtinTypes->neverType, superTy); + result = isCovariantWith(env, builtinTypes->neverType, superTy).withSubComponent(TypePath::TypeField::Negated); } else if (is(negatedTy)) { // ยฌany ~ any - result = isCovariantWith(env, negatedTy, superTy); + result = isCovariantWith(env, negatedTy, superTy).withSubComponent(TypePath::TypeField::Negated); } else if (auto u = get(negatedTy)) { @@ -808,8 +911,13 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const Nega for (TypeId ty : u) { - NegationType negatedTmp{ty}; - subtypings.push_back(isCovariantWith(env, &negatedTmp, superTy)); + if (auto negatedPart = get(follow(ty))) + subtypings.push_back(isCovariantWith(env, negatedPart->ty, superTy).withSubComponent(TypePath::TypeField::Negated)); + else + { + NegationType negatedTmp{ty}; + subtypings.push_back(isCovariantWith(env, &negatedTmp, superTy)); + } } result = SubtypingResult::all(subtypings); @@ -823,7 +931,7 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const Nega for (TypeId ty : i) { if (auto negatedPart = get(follow(ty))) - subtypings.push_back(isCovariantWith(env, negatedPart->ty, superTy)); + subtypings.push_back(isCovariantWith(env, negatedPart->ty, superTy).withSubComponent(TypePath::TypeField::Negated)); else { NegationType negatedTmp{ty}; @@ -841,10 +949,10 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const Nega // subtype of other stuff. else { - result = {false}; + result = SubtypingResult{false}.withSubComponent(TypePath::TypeField::Negated); } - return result.withSubComponent(TypePath::TypeField::Negated); + return result; } SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const TypeId subTy, const NegationType* superNegation) @@ -885,7 +993,7 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const Type } } - result = SubtypingResult::all(subtypings); + return SubtypingResult::all(subtypings); } else if (auto i = get(negatedTy)) { @@ -904,7 +1012,7 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const Type } } - result = SubtypingResult::any(subtypings); + return SubtypingResult::any(subtypings); } else if (auto p = get2(subTy, negatedTy)) { @@ -986,8 +1094,7 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const Tabl { std::vector results; if (auto it = subTable->props.find(name); it != subTable->props.end()) - results.push_back(isInvariantWith(env, it->second.type(), prop.type()) - .withBothComponent(TypePath::Property(name))); + results.push_back(isInvariantWith(env, it->second.type(), prop.type()).withBothComponent(TypePath::Property(name))); if (subTable->indexer) { @@ -1122,7 +1229,7 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const Tabl { return isInvariantWith(env, subIndexer.indexType, superIndexer.indexType) .withBothComponent(TypePath::TypeField::IndexLookup) - .andAlso(isInvariantWith(env, superIndexer.indexResultType, subIndexer.indexResultType).withBothComponent(TypePath::TypeField::IndexResult)); + .andAlso(isInvariantWith(env, subIndexer.indexResultType, superIndexer.indexResultType).withBothComponent(TypePath::TypeField::IndexResult)); } SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const NormalizedType* subNorm, const NormalizedType* superNorm) @@ -1249,12 +1356,11 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const Type { std::vector results; - size_t i = 0; for (TypeId subTy : subTypes) { results.emplace_back(); for (TypeId superTy : superTypes) - results.back().orElse(isCovariantWith(env, subTy, superTy).withBothComponent(TypePath::Index{i++})); + results.back().orElse(isCovariantWith(env, subTy, superTy)); } return SubtypingResult::all(results); diff --git a/Analysis/src/ToString.cpp b/Analysis/src/ToString.cpp index 4f7869d0..918da330 100644 --- a/Analysis/src/ToString.cpp +++ b/Analysis/src/ToString.cpp @@ -1721,7 +1721,7 @@ std::string toString(const Constraint& constraint, ToStringOptions& opts) std::string iteratorStr = tos(c.iterator); std::string variableStr = tos(c.variables); - return variableStr + " ~ Iterate<" + iteratorStr + ">"; + return variableStr + " ~ iterate " + iteratorStr; } else if constexpr (std::is_same_v) { diff --git a/Analysis/src/TypeChecker2.cpp b/Analysis/src/TypeChecker2.cpp index 0250817c..32b91637 100644 --- a/Analysis/src/TypeChecker2.cpp +++ b/Analysis/src/TypeChecker2.cpp @@ -2467,6 +2467,8 @@ struct TypeChecker2 std::string relation = "a subtype of"; if (reasoning.variance == SubtypingVariance::Invariant) relation = "exactly"; + else if (reasoning.variance == SubtypingVariance::Contravariant) + relation = "a supertype of"; std::string reason; if (reasoning.subPath == reasoning.superPath) diff --git a/Analysis/src/TypePath.cpp b/Analysis/src/TypePath.cpp index 9b470a2a..fb4d68cb 100644 --- a/Analysis/src/TypePath.cpp +++ b/Analysis/src/TypePath.cpp @@ -12,7 +12,6 @@ #include #include #include -#include LUAU_FASTFLAG(DebugLuauReadWriteProperties); @@ -252,8 +251,6 @@ struct TraversalState TypeOrPack current; NotNull builtinTypes; - - DenseHashSet seen{nullptr}; int steps = 0; void updateCurrent(TypeId ty) @@ -268,18 +265,6 @@ struct TraversalState current = follow(tp); } - bool haveCycle() - { - const void* currentPtr = ptr(current); - - if (seen.contains(currentPtr)) - return true; - else - seen.insert(currentPtr); - - return false; - } - bool tooLong() { return ++steps > DFInt::LuauTypePathMaximumTraverseSteps; @@ -287,7 +272,7 @@ struct TraversalState bool checkInvariants() { - return haveCycle() || tooLong(); + return tooLong(); } bool traverse(const TypePath::Property& property) @@ -313,18 +298,36 @@ struct TraversalState { prop = lookupClassProp(c, property.name); } - else if (auto m = getMetatable(*currentType, builtinTypes)) + // For a metatable type, the table takes priority; check that before + // falling through to the metatable entry below. + else if (auto m = get(*currentType)) { - // Weird: rather than use findMetatableEntry, which requires a lot - // of stuff that we don't have and don't want to pull in, we use the - // path traversal logic to grab __index and then re-enter the lookup - // logic there. - updateCurrent(*m); + TypeOrPack pinned = current; + updateCurrent(m->table); - if (!traverse(TypePath::Property{"__index"})) - return false; + if (traverse(property)) + return true; - return traverse(property); + // Restore the old current type if we didn't traverse the metatable + // successfully; we'll use the next branch to address this. + current = pinned; + } + + if (!prop) + { + if (auto m = getMetatable(*currentType, builtinTypes)) + { + // Weird: rather than use findMetatableEntry, which requires a lot + // of stuff that we don't have and don't want to pull in, we use the + // path traversal logic to grab __index and then re-enter the lookup + // logic there. + updateCurrent(*m); + + if (!traverse(TypePath::Property{"__index"})) + return false; + + return traverse(property); + } } if (prop) diff --git a/Analysis/src/Unifier2.cpp b/Analysis/src/Unifier2.cpp index 41a5afb0..6b213aea 100644 --- a/Analysis/src/Unifier2.cpp +++ b/Analysis/src/Unifier2.cpp @@ -113,7 +113,7 @@ bool Unifier2::unify(TypeId subTy, TypeId superTy) return argResult && retResult; } - auto subTable = get(subTy); + auto subTable = getMutable(subTy); auto superTable = get(superTy); if (subTable && superTable) { @@ -210,7 +210,7 @@ bool Unifier2::unify(TypeId subTy, const IntersectionType* superIntersection) return result; } -bool Unifier2::unify(const TableType* subTable, const TableType* superTable) +bool Unifier2::unify(TableType* subTable, const TableType* superTable) { bool result = true; @@ -256,6 +256,21 @@ bool Unifier2::unify(const TableType* subTable, const TableType* superTable) result &= unify(subTable->indexer->indexResultType, superTable->indexer->indexResultType); } + if (!subTable->indexer && subTable->state == TableState::Unsealed && superTable->indexer) + { + /* + * Unsealed tables are always created from literal table expressions. We + * can't be completely certain whether such a table has an indexer just + * by the content of the expression itself, so we need to be a bit more + * flexible here. + * + * If we are trying to reconcile an unsealed table with a table that has + * an indexer, we therefore conclude that the unsealed table has the + * same indexer. + */ + subTable->indexer = *superTable->indexer; + } + return result; } diff --git a/Ast/include/Luau/Ast.h b/Ast/include/Luau/Ast.h index ad5592f5..2abda788 100644 --- a/Ast/include/Luau/Ast.h +++ b/Ast/include/Luau/Ast.h @@ -3,6 +3,7 @@ #include "Luau/Location.h" +#include #include #include #include @@ -91,10 +92,21 @@ struct AstArray { return data; } + const T* end() const { return data + size; } + + std::reverse_iterator rbegin() const + { + return std::make_reverse_iterator(end()); + } + + std::reverse_iterator rend() const + { + return std::make_reverse_iterator(begin()); + } }; struct AstTypeList diff --git a/Ast/src/Parser.cpp b/Ast/src/Parser.cpp index 510e5f0e..e4b840b2 100644 --- a/Ast/src/Parser.cpp +++ b/Ast/src/Parser.cpp @@ -19,8 +19,6 @@ LUAU_FASTINTVARIABLE(LuauParseErrorLimit, 100) LUAU_FASTFLAGVARIABLE(LuauClipExtraHasEndProps, false) LUAU_FASTFLAG(LuauCheckedFunctionSyntax) -LUAU_FASTFLAGVARIABLE(LuauParseImpreciseNumber, false) - namespace Luau { @@ -2168,11 +2166,8 @@ static ConstantNumberParseResult parseInteger(double& result, const char* data, return base == 2 ? ConstantNumberParseResult::BinOverflow : ConstantNumberParseResult::HexOverflow; } - if (FFlag::LuauParseImpreciseNumber) - { - if (value >= (1ull << 53) && static_cast(result) != value) - return ConstantNumberParseResult::Imprecise; - } + if (value >= (1ull << 53) && static_cast(result) != value) + return ConstantNumberParseResult::Imprecise; return ConstantNumberParseResult::Ok; } @@ -2190,32 +2185,24 @@ static ConstantNumberParseResult parseDouble(double& result, const char* data) char* end = nullptr; double value = strtod(data, &end); - if (FFlag::LuauParseImpreciseNumber) + // trailing non-numeric characters + if (*end != 0) + return ConstantNumberParseResult::Malformed; + + result = value; + + // for linting, we detect integer constants that are parsed imprecisely + // since the check is expensive we only perform it when the number is larger than the precise integer range + if (value >= double(1ull << 53) && strspn(data, "0123456789") == strlen(data)) { - // trailing non-numeric characters - if (*end != 0) - return ConstantNumberParseResult::Malformed; + char repr[512]; + snprintf(repr, sizeof(repr), "%.0f", value); - result = value; - - // for linting, we detect integer constants that are parsed imprecisely - // since the check is expensive we only perform it when the number is larger than the precise integer range - if (value >= double(1ull << 53) && strspn(data, "0123456789") == strlen(data)) - { - char repr[512]; - snprintf(repr, sizeof(repr), "%.0f", value); - - if (strcmp(repr, data) != 0) - return ConstantNumberParseResult::Imprecise; - } - - return ConstantNumberParseResult::Ok; - } - else - { - result = value; - return *end == 0 ? ConstantNumberParseResult::Ok : ConstantNumberParseResult::Malformed; + if (strcmp(repr, data) != 0) + return ConstantNumberParseResult::Imprecise; } + + return ConstantNumberParseResult::Ok; } // simpleexp -> NUMBER | STRING | NIL | true | false | ... | constructor | FUNCTION body | primaryexp diff --git a/CLI/Compile.cpp b/CLI/Compile.cpp index bc1d5c60..95043271 100644 --- a/CLI/Compile.cpp +++ b/CLI/Compile.cpp @@ -37,13 +37,19 @@ enum class RecordStats { None, Total, - Split + File, + Function }; struct GlobalOptions { int optimizationLevel = 1; int debugLevel = 1; + + std::string vectorLib; + std::string vectorCtor; + std::string vectorType; + } globalOptions; static Luau::CompileOptions copts() @@ -52,6 +58,11 @@ static Luau::CompileOptions copts() result.optimizationLevel = globalOptions.optimizationLevel; result.debugLevel = globalOptions.debugLevel; + // globalOptions outlive the CompileOptions, so it's safe to use string data pointers here + result.vectorLib = globalOptions.vectorLib.c_str(); + result.vectorCtor = globalOptions.vectorCtor.c_str(); + result.vectorType = globalOptions.vectorType.c_str(); + return result; } @@ -159,11 +170,26 @@ struct CompileStats \"blockLinearizationStats\": {\ \"constPropInstructionCount\": %u, \ \"timeSeconds\": %f\ -}}", +}", lines, bytecode, bytecodeInstructionCount, codegen, readTime, miscTime, parseTime, compileTime, codegenTime, lowerStats.totalFunctions, lowerStats.skippedFunctions, lowerStats.spillsToSlot, lowerStats.spillsToRestore, lowerStats.maxSpillSlotsUsed, lowerStats.blocksPreOpt, lowerStats.blocksPostOpt, lowerStats.maxBlockInstructions, lowerStats.regAllocErrors, lowerStats.loweringErrors, lowerStats.blockLinearizationStats.constPropInstructionCount, lowerStats.blockLinearizationStats.timeSeconds); + if (lowerStats.collectFunctionStats) + { + fprintf(fp, ", \"functions\": ["); + auto functionCount = lowerStats.functions.size(); + for (size_t i = 0; i < functionCount; ++i) + { + const Luau::CodeGen::FunctionStats& fstat = lowerStats.functions[i]; + fprintf(fp, "{\"name\": \"%s\", \"line\": %d, \"bcodeCount\": %u, \"irCount\": %u, \"asmCount\": %u}", fstat.name.c_str(), fstat.line, + fstat.bcodeCount, fstat.irCount, fstat.asmCount); + if (i < functionCount - 1) + fprintf(fp, ", "); + } + fprintf(fp, "]"); + } + fprintf(fp, "}"); } CompileStats& operator+=(const CompileStats& that) @@ -321,7 +347,11 @@ static void displayHelp(const char* argv0) printf(" -g: compile with debug level n (default 1, n should be between 0 and 2).\n"); printf(" --target=: compile code for specific architecture (a64, x64, a64_nf, x64_ms).\n"); printf(" --timetrace: record compiler time tracing information into trace.json\n"); - printf(" --record-stats=