From a0b9950541e730dd3095821bb8623c1ba9c09e70 Mon Sep 17 00:00:00 2001 From: Aaron Weiss Date: Fri, 3 Nov 2023 12:47:28 -0700 Subject: [PATCH 001/116] Sync to upstream/release/602 --- ...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} | 247 ++++++++---------- 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 | 94 +------ 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 | 21 +- 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, 1017 insertions(+), 582 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 bba5ebd93..088ae4c22 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 @@ struct ConstraintGraphBuilder 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 @@ struct ConstraintGraphBuilder /** 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 34a0484a3..f752f0222 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 @@ struct DataFlowGraph // 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 ebb80e0f7..54a4dc619 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 aeeab0f8a..b30cfe01b 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 2bdca1df4..870019109 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 + return nullptr; +} + +template, bool> = true> +const T* get(const TypeOrPack& tyOrTp) +{ + if (const TypePackId* tp = get(tyOrTp)) + return get(*tp); else - { - static_assert(always_false_v, "invalid T to get from TypeOrPack"); - LUAU_UNREACHABLE(); - } + return nullptr; } TypeOrPack follow(TypeOrPack ty); diff --git a/Analysis/src/Autocomplete.cpp b/Analysis/src/Autocomplete.cpp index d73598c75..52cb54e34 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 b7631460a..5ce128731 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 01b0bdfd5..a0e76987d 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 @@ class TypeCloner2 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 @@ class TypeCloner2 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 4f4ff3065..e56525496 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 e39335742..3f78f3a6e 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 6cbc19fa0..feea40c45 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 e957eee78..4aef48c3a 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 @@ class LintDeprecatedApi : AstVisitor // 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 @@ class LintTableOperations : AstVisitor 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 @@ class LintTableOperations : AstVisitor { 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 @@ class LintTableOperations : AstVisitor 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 @@ class LintIntegerParsing : AstVisitor 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 580f59f3e..d50719a9e 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 52bbc5d9c..c21f7f325 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 e386bf7b9..6e386e68b 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 bf8e362dc..bd6374534 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 a29b1e06e..a4734dbd7 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 c371e81ed..0940ea922 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 a3908a561..ad5592f50 100644 --- a/Ast/include/Luau/Ast.h +++ b/Ast/include/Luau/Ast.h @@ -249,6 +249,7 @@ class AstExprConstantBool : public AstExpr enum class ConstantNumberParseResult { Ok, + Imprecise, Malformed, BinOverflow, HexOverflow, diff --git a/Ast/include/Luau/Location.h b/Ast/include/Luau/Location.h index 041a2c631..3fc8921a5 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 40f8e23ee..c2c66d9f2 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 a97471438..3871ea628 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 4f6b54b65..bc1d5c602 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 409bc22aa..dfa3eeb0f 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 19e082b5b..9beee0ac8 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 8fcd832fb..484d2dabe 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 3cdd20b35..9306ae4c7 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)]; - - 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)); - } + // 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 + + // 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 7075e3482..515a81f0a 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 483e3e002..fd015b3a0 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 26a3b887a..98beb5130 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 b9ff4f1fa..df7488a91 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 3b6b5def6..b7d66ae69 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 ba46dbc98..5e6064819 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 7b2f068b2..a161987de 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 f0b8561c8..7670482d1 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 3315ec962..a37b810d5 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 f5098d176..2d86e4127 100644 --- a/Compiler/include/Luau/BytecodeBuilder.h +++ b/Compiler/include/Luau/BytecodeBuilder.h @@ -83,6 +83,7 @@ class BytecodeBuilder 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 @@ class BytecodeBuilder 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 83fb9ce5f..7f5452820 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 e0a0cac8d..c685ffbd0 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 2604514ed..fb883d7d9 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 04852e878..e28bb1698 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; } @@ -1374,10 +1374,11 @@ static int luauF_writeinteger(lua_State* L, StkId res, TValue* arg0, int nresult return -1; unsigned value; - luai_num2unsigned(value, nvalue(args + 1)); + double incoming = nvalue(args + 1); + 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 @@ -1397,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; } @@ -1418,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 6729f155b..d13e98f37 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 0e4227a3f..a01e801ed 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 bc9a12ea4..35dfd230f 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 b7f77711f..2a2017a6d 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 293c26ff9..dc1aea808 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 5e7fedab5..ff362be11 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 32cb3cda0..204d4d147 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 0a71794f2..a1869e884 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 4388c4005..bd69cb13f 100644 --- a/tests/IrBuilder.test.cpp +++ b/tests/IrBuilder.test.cpp @@ -3106,3 +3106,37 @@ TEST_CASE_FIXTURE(IrBuilderFixture, "TagSelfEqualityCheckRemoval") } 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 9907d7a16..f71d92ad1 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 208db5d8f..c966968fe 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 ee818022d..30ec33167 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 9dc193d88..57455244c 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 3f6d90fa9..d11e89888 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 19700d2c8..07652c15f 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 94eec9614..a6d3fa5cb 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 18b8ab8ae..6892c78f7 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 a332dee2f..7fe4a2c31 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 97ee15e1a..23314535a 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 d5ae004cf..b0a8b98d8 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 a2a28ae52..a1d5e95a4 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 cc925cabe..9f523ce34 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 53127c3de..5d4a49bfb 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 281ad2744..f394dc5b4 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 d285df780..9262f4ea5 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 bfd7a1ac8..3314216fd 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 96a6525f0..57d4ee7c6 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 46454bbc7..ca8112a1c 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 4b68791b2c961958a30bfc887a5b5ead7987bdff Mon Sep 17 00:00:00 2001 From: Alexander McCord Date: Fri, 10 Nov 2023 10:05:48 -0800 Subject: [PATCH 002/116] Sync to upstream/release/603 --- Analysis/include/Luau/ConstraintGenerator.h | 2 + Analysis/include/Luau/ConstraintSolver.h | 10 +- Analysis/include/Luau/DataFlowGraph.h | 59 +-- 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 | 18 +- 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 | 391 +++++++++++++++----- 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 | 12 +- Analysis/src/TypeAttach.cpp | 6 +- Analysis/src/TypeChecker2.cpp | 122 ++++-- Analysis/src/TypeFamily.cpp | 14 +- Analysis/src/TypeInfer.cpp | 46 ++- 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 | 129 ++----- 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 | 23 ++ 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 | 134 +++---- 113 files changed, 3631 insertions(+), 1082 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 088ae4c22..aab31c401 100644 --- a/Analysis/include/Luau/ConstraintGenerator.h +++ b/Analysis/include/Luau/ConstraintGenerator.h @@ -148,6 +148,8 @@ struct ConstraintGenerator */ 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 a0afeed79..4a4d639b4 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 f752f0222..ab957b89d 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 @@ struct DataFlowGraph 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 @@ struct DataFlowGraphBuilder std::vector> scopes; - DfgScope* childScope(DfgScope* scope); - - void visit(DfgScope* scope, AstStatBlock* b); - void 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); + DfgScope* childScope(DfgScope* scope, bool isLoopScope = false); + void join(DfgScope* parent, DfgScope* a, DfgScope* b); + + ControlFlow visit(DfgScope* scope, AstStatBlock* b); + ControlFlow visitBlockWithoutChildScope(DfgScope* scope, AstStatBlock* b); + + ControlFlow visit(DfgScope* scope, AstStat* s); + ControlFlow visit(DfgScope* scope, AstStatIf* i); + ControlFlow visit(DfgScope* scope, AstStatWhile* w); + ControlFlow visit(DfgScope* scope, AstStatRepeat* r); + ControlFlow visit(DfgScope* scope, AstStatBreak* b); + ControlFlow visit(DfgScope* scope, AstStatContinue* c); + ControlFlow visit(DfgScope* scope, AstStatReturn* r); + ControlFlow visit(DfgScope* scope, AstStatExpr* e); + ControlFlow visit(DfgScope* scope, AstStatLocal* l); + ControlFlow visit(DfgScope* scope, AstStatFor* f); + ControlFlow visit(DfgScope* scope, AstStatForIn* f); + ControlFlow visit(DfgScope* scope, AstStatAssign* a); + ControlFlow visit(DfgScope* scope, AstStatCompoundAssign* c); + ControlFlow visit(DfgScope* scope, AstStatFunction* f); + ControlFlow visit(DfgScope* scope, AstStatLocalFunction* l); + ControlFlow visit(DfgScope* scope, AstStatTypeAlias* t); + ControlFlow visit(DfgScope* scope, AstStatDeclareGlobal* d); + ControlFlow visit(DfgScope* scope, AstStatDeclareFunction* d); + ControlFlow visit(DfgScope* scope, AstStatDeclareClass* d); + ControlFlow visit(DfgScope* scope, AstStatError* error); DataFlowResult visitExpr(DfgScope* scope, AstExpr* e); DataFlowResult visitExpr(DfgScope* scope, AstExprGroup* group); diff --git a/Analysis/include/Luau/Def.h b/Analysis/include/Luau/Def.h index 0a286ae98..0a85fdeee 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 4b6c64c33..ddbf6dcb5 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 25af52005..2b83c4436 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 @@ struct Frontend 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 d647750fc..197c7f9c7 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 54a4dc619..4508d4a4f 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 @@ class Normalizer 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 @@ class Normalizer 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 8cdffcb49..2360c986e 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 000000000..5baff136a --- /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 064648d73..10f27d4e2 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 321563e7c..cb2d48dd3 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 9b8564fb8..51d2ded1a 100644 --- a/Analysis/include/Luau/Type.h +++ b/Analysis/include/Luau/Type.h @@ -21,7 +21,6 @@ #include #include #include -#include #include LUAU_FASTINT(LuauTableTypeMaximumStringifierLength) @@ -141,6 +140,7 @@ struct PrimitiveType Thread, Function, Table, + Buffer, }; Type type; @@ -373,7 +373,15 @@ struct Property bool deprecated = false; std::string deprecatedSuggestion; + + // If this property was inferred from an expression, this field will be + // populated with the source location of the corresponding table property. std::optional location = std::nullopt; + + // If this property was built from an explicit type annotation, this field + // will be populated with the source location of that table property. + std::optional typeLocation = std::nullopt; + Tags tags; std::optional documentationSymbol; @@ -381,7 +389,7 @@ struct Property // TODO: Kill all constructors in favor of `Property::rw(TypeId read, TypeId write)` and friends. Property(); Property(TypeId readTy, bool deprecated = false, const std::string& deprecatedSuggestion = "", std::optional location = std::nullopt, - const Tags& tags = {}, const std::optional& documentationSymbol = std::nullopt); + const Tags& tags = {}, const std::optional& documentationSymbol = std::nullopt, std::optional typeLocation = std::nullopt); // DEPRECATED: Should only be called in non-RWP! We assert that the `readTy` is not nullopt. // TODO: Kill once we don't have non-RWP. @@ -739,6 +747,7 @@ bool isBoolean(TypeId ty); bool isNumber(TypeId ty); bool isString(TypeId ty); bool isThread(TypeId ty); +bool isBuffer(TypeId ty); bool isOptional(TypeId ty); bool isTableIntersection(TypeId ty); bool isOverloadedFunction(TypeId ty); @@ -797,6 +806,7 @@ struct BuiltinTypes const TypeId stringType; const TypeId booleanType; const TypeId threadType; + const TypeId bufferType; const TypeId functionType; const TypeId classType; const TypeId tableType; @@ -965,7 +975,7 @@ struct TypeIterator using SavedIterInfo = std::pair; std::deque stack; - std::unordered_set seen; // Only needed to protect the iterator from hanging the thread. + DenseHashSet seen{nullptr}; // Only needed to protect the iterator from hanging the thread. void advance() { @@ -992,7 +1002,7 @@ struct TypeIterator { // 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 7de014068..26a67c7a0 100644 --- a/Analysis/include/Luau/TypeInfer.h +++ b/Analysis/include/Luau/TypeInfer.h @@ -377,6 +377,7 @@ struct TypeChecker 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 96fcdcb15..bdca95a4c 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 3d5b5a1a1..49a275d51 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 920517c28..2d1940d47 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 a0e76987d..1b97bb892 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; - - std::vector options; - options.reserve(t.options.size()); - - for (TypeId ty : t.options) - options.push_back(clone(ty, dest, cloneState)); + // 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; - asMutable(result)->ty.emplace(std::move(options)); - } - else - { - 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)); - 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 e56525496..15e64c920 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 3b4784948..c056a150f 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 3f78f3a6e..60d959867 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 d34b5cdc7..fdbc089f3 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}; + + while (!queue.empty()) + { + DefId next = queue.front(); + queue.pop_front(); + + LUAU_ASSERT(!seen.find(next)); + if (seen.find(next)) + continue; + seen.insert(next); - LUAU_ASSERT(!"Phi nodes not implemented yet"); + 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 632f8e5c0..874f86279 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 db6a240a3..3be63f02a 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 feea40c45..710e36993 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 654cfa5d5..1c96ad702 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 4aef48c3a..ea35a6a1f 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 @@ class LintUnknownType : AstVisitor 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 @@ class LintTableOperations : AstVisitor 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 595794a0f..5ff782ea3 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); - } - - 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) - { - visit(expr->expr, context); - } - 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) + visit(statement); + return {}; + } + + 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 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"); + } + } + + 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 {}; } - 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) {} + NonStrictContext visit(AstExprVarargs* global) + { + return {}; + } - void visit(AstExprCall* call, NonStrictContext& context) + + 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; + } + + NonStrictContext visit(AstExprIndexName* indexName) + { + return {}; } - void visit(AstExprIndexName* indexName, NonStrictContext& context) {} - void visit(AstExprIndexExpr* indexExpr, NonStrictContext& context) {} - void visit(AstExprFunction* exprFn, NonStrictContext& context) + 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 c21f7f325..9f14b355e 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 2ca40bdd9..6beffc2cd 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 6519c6ff0..d04aeb820 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 6e386e68b..f45f6d3e4 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 58c03db43..cc01d626f 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 8fd407724..85b8849f7 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 1859131bf..e76837a8d 100644 --- a/Analysis/src/Type.cpp +++ b/Analysis/src/Type.cpp @@ -27,6 +27,7 @@ LUAU_FASTINT(LuauTypeInferRecursionLimit) LUAU_FASTFLAG(LuauInstantiateInSubtyping) LUAU_FASTFLAG(DebugLuauReadWriteProperties) LUAU_FASTFLAGVARIABLE(LuauInitializeStringMetatableInGlobalTypes, false) +LUAU_FASTFLAG(LuauBufferTypeck) namespace Luau { @@ -214,6 +215,13 @@ bool isThread(TypeId ty) return isPrim(ty, PrimitiveType::Thread); } +bool isBuffer(TypeId ty) +{ + LUAU_ASSERT(FFlag::LuauBufferTypeck); + + return isPrim(ty, PrimitiveType::Buffer); +} + bool isOptional(TypeId ty) { if (isNil(ty)) @@ -604,10 +612,11 @@ FunctionType::FunctionType(TypeLevel level, Scope* scope, std::vector ge Property::Property() {} Property::Property(TypeId readTy, bool deprecated, const std::string& deprecatedSuggestion, std::optional location, const Tags& tags, - const std::optional& documentationSymbol) + const std::optional& documentationSymbol, std::optional typeLocation) : deprecated(deprecated) , deprecatedSuggestion(deprecatedSuggestion) , location(location) + , typeLocation(typeLocation) , tags(tags) , documentationSymbol(documentationSymbol) , readTy(readTy) @@ -925,6 +934,7 @@ BuiltinTypes::BuiltinTypes() , stringType(arena->addType(Type{PrimitiveType{PrimitiveType::String}, /*persistent*/ true})) , booleanType(arena->addType(Type{PrimitiveType{PrimitiveType::Boolean}, /*persistent*/ true})) , threadType(arena->addType(Type{PrimitiveType{PrimitiveType::Thread}, /*persistent*/ true})) + , bufferType(arena->addType(Type{PrimitiveType{PrimitiveType::Buffer}, /*persistent*/ true})) , functionType(arena->addType(Type{PrimitiveType{PrimitiveType::Function}, /*persistent*/ true})) , classType(arena->addType(Type{ClassType{"class", {}, std::nullopt, std::nullopt, {}, {}, {}}, /*persistent*/ true})) , tableType(arena->addType(Type{PrimitiveType{PrimitiveType::Table}, /*persistent*/ true})) diff --git a/Analysis/src/TypeAttach.cpp b/Analysis/src/TypeAttach.cpp index 3a1217bfc..fb47471c2 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 @@ class TypeRehydrationVisitor 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 @@ class TypeRehydrationVisitor } 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 bd6374534..8df78140f 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 e3afb9441..a3a67ace6 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 a4734dbd7..0ffe40df6 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) + { + // 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 { - reportError(expr.location, GenericError{format("Type '%s' cannot be compared with relational operator %s", toString(leftType).c_str(), - toString(expr.op).c_str())}); + 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; @@ -5394,7 +5400,7 @@ TypeId TypeChecker::resolveTypeWorker(const ScopePtr& scope, const AstType& anno std::optional tableIndexer; for (const auto& prop : table->props) - props[prop.name.value] = {resolveType(scope, *prop.type)}; + props[prop.name.value] = {resolveType(scope, *prop.type), /* deprecated: */ false, {}, std::nullopt, {}, std::nullopt, prop.location}; if (const auto& indexer = table->indexer) tableIndexer = TableIndexer(resolveType(scope, *indexer->indexType), resolveType(scope, *indexer->resultType)); @@ -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 ff515bed1..9b470a2a2 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 0940ea922..93f8a8513 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 11f96ea1a..41a5afb08 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 a7d7eef7e..9a6ca4d72 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 a493acfe4..96653a56a 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(); + consume(); - if (ch == '=') - { - consume(); - return Lexeme(Location(start, 2), Lexeme::DivAssign); - } - else if (ch == '/') - { - consume(); + char ch = peekch(); - if (peekch() == '=') - { - consume(); - return Lexeme(Location(start, 3), Lexeme::FloorDivAssign); - } - else - return Lexeme(Location(start, 2), Lexeme::FloorDiv); - } - else - return Lexeme(Location(start, 1), '/'); + if (ch == '=') + { + consume(); + 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 3871ea628..510e5f0e1 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 8a31e6808..65d3ce0ac 100644 --- a/CodeGen/include/Luau/AssemblyBuilderX64.h +++ b/CodeGen/include/Luau/AssemblyBuilderX64.h @@ -133,6 +133,7 @@ class AssemblyBuilderX64 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 @@ class AssemblyBuilderX64 void vblendvpd(RegisterX64 dst, RegisterX64 src1, OperandX64 mask, RegisterX64 src3); - // Run final checks bool finalize(); @@ -228,6 +228,7 @@ class AssemblyBuilderX64 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 9beee0ac8..33cae51ca 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 3fe1800b6..06c874484 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 6fdeac278..22978dd4a 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 70e27097f..2c70ef6c0 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 fd015b3a0..7893d076a 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 98beb5130..42450d3c0 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 46f410210..5fb7f2b8a 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 df7488a91..f7572a6cd 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 920ad0025..5f12b303b 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 b7d66ae69..6aec01a5e 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); + + // 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()); + 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 763a84782..dff7002d4 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)) + { + case LCT_VAL: { - 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 - } + 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; } - else + + 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; + } - 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))); - 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 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 5e6064819..15cd94263 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 20dee34a5..b17be6821 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 a37b810d5..8d0f829a9 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 72aa6ec5f..067a9d7a6 100644 --- a/Common/include/Luau/DenseHash.h +++ b/Common/include/Luau/DenseHash.h @@ -539,6 +539,25 @@ class DenseHashSet { 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 7f5452820..296cf4ef9 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 c685ffbd0..8722fe8d6 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 8e9802cf9..97d86b62f 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 2e5e97910..9e97633f6 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 fb883d7d9..929c99a4d 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 51ed5dac2..3fb6e1660 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 1dbd34c62..a3365558e 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 4887b5e80..ef99b94fe 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 5eecc2acb..451433ee7 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 @@ static void luau_execute(lua_State* L) { // 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 000000000..a1760f677 --- /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 63c7618f2..ba6fb4c8c 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 ccf1ca173..c55de91a9 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 35dfd230f..922a35052 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 2a2017a6d..968a55be8 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 cd91039b9..e957316e1 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 fded0715c..6b7a6558f 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 fa3dfb067..87073b2b6 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 613334228..3c584c48b 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 e0756badd..51122f380 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 bd69cb13f..1b7242e22 100644 --- a/tests/IrBuilder.test.cpp +++ b/tests/IrBuilder.test.cpp @@ -1933,8 +1933,6 @@ TEST_CASE_FIXTURE(IrBuilderFixture, "LoadPropagatesOnlyRightType") 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 @@ TEST_CASE_FIXTURE(IrBuilderFixture, "DuplicateHashSlotChecks") 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 @@ TEST_CASE_FIXTURE(IrBuilderFixture, "TagStoreUpdatesSetUpval") 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 f71d92ad1..7f6431d96 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 c966968fe..c596b5b80 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 30ec33167..54e77532b 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 0ee135a14..301474028 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 000000000..4476452a8 --- /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 cb3335626..0c222f292 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 57455244c..d7120bcac 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 6c667ee62..00c3c737f 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 ae7a925c0..871d984ee 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 d11e89888..199b1b229 100644 --- a/tests/TypeInfer.aliases.test.cpp +++ b/tests/TypeInfer.aliases.test.cpp @@ -1035,4 +1035,27 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "alias_expands_to_bare_reference_to_imported_ LUAU_REQUIRE_NO_ERRORS(result); } +TEST_CASE_FIXTURE(Fixture, "table_types_record_the_property_locations") +{ + CheckResult result = check(R"( + type Table = { + create: () -> () + } + + local x: Table + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + auto ty = requireTypeAlias("Table"); + + auto ttv = Luau::get(follow(ty)); + REQUIRE(ttv); + + auto propIt = ttv->props.find("create"); + REQUIRE(propIt != ttv->props.end()); + + CHECK_EQ(propIt->second.location, std::nullopt); + CHECK_EQ(propIt->second.typeLocation, Location({2, 12}, {2, 18})); +} + TEST_SUITE_END(); diff --git a/tests/TypeInfer.annotations.test.cpp b/tests/TypeInfer.annotations.test.cpp index 035344135..99c0b088d 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 2c68ae94b..1e6150a3c 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 615b81d6f..86f619fdf 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 1e9b8ad9b..da207c49d 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 75f1ce035..ba3c82165 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 55bec5af3..cba1f37e5 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 23314535a..1bc6b380a 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 b0a8b98d8..8e32a6a7c 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 a1d5e95a4..5af349308 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 8efa8303d..8aa426533 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 c15d5c0f1..cee368329 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 fc7f9707e..9077167e1 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 a6b951ea0..1cf996da5 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 3314216fd..c86e90eb5 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 57d4ee7c6..fa6d61b53 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 ca8112a1c..c421f2ce5 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 000000000..742e47fec --- /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 000000000..18e715fe3 --- /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 000000000..0f591a2b3 --- /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 000000000..85f74c106 --- /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 92891a0cd..000000000 --- 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 661b20fc1..30654af36 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 @@ def has_children(self): 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 @@ def get_child_at_index(self, index): 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(), - ) - - except Exception as e: - print("get_child_at_index error", e, index) + 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") - 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") + 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 - if child_key_valobj.TypeIsPointerType() and child_key_valobj.GetValueAsUnsigned() == 0: + # Skip over previous occupied slots. + if index > skipped: + skipped += 1 continue - child_key = child_key_valobj.GetValue() + slot_key = slot_key_valobj.GetSummary() + if slot_key is None: + slot_key = slot_key_valobj.GetValue() - if child_key is None: - child_key = child_key_valobj.GetSummary() + if slot_key is None: + slot_key = slot_key_valobj.GetValueAsSigned() - if child_key is None: - child_key = f"<{index} ({child_key_valobj.GetTypeName()})>" + if slot_key is None: + slot_key = slot_key_valobj.GetValueAsUnsigned() - child_value = child_pair.GetChildMemberWithName("second") - self.items.append({"key": child_key, "value": child_value}) + if slot_key is None: + slot_key = str(index) - self.count = len(self.items) + 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: + 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 @@ def has_children(self): 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 @@ def get_child_at_index(self, index): 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(), - ) - - except Exception as e: - print("get_child_at_index error", e, index) + for slot in range(0, min(self.max_expand_capacity, self.capacity)): + slot_valobj = self.valobj.GetValueForExpressionPath( + f".impl.data[{slot}]") - 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}]") + 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 - if child_value.TypeIsPointerType() and child_value.GetValueAsUnsigned() == 0: + # Skip over previous occupied slots. + if index > skipped: + skipped += 1 continue - self.items.append(child_value) + return self.valobj.CreateValueFromData(f"[{index}]", slot_valobj.GetData(), slot_valobj.GetType()) - self.count = len(self.items) + except Exception as e: + print("get_child_at_index error", e, index) + def update(self): + try: + 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 674c6c40c04e39fb2318667181e0212bfb2d1139 Mon Sep 17 00:00:00 2001 From: Andy Friesen Date: Fri, 17 Nov 2023 10:15:31 -0800 Subject: [PATCH 003/116] Sync to upstream/release/604 --- 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 | 141 ++++++--- Analysis/src/DataFlowGraph.cpp | 137 ++++++-- Analysis/src/Def.cpp | 56 ++-- 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, 1734 insertions(+), 371 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 ad10ca990..a026fdae6 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 aab31c401..69b0fd203 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 @@ struct ConstraintGenerator 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 @@ struct ConstraintGenerator */ 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 ab957b89d..083e50460 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 @@ struct DataFlowGraphBuilder 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 0a85fdeee..e3fec9b67 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 2360c986e..3f2b73558 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 5baff136a..3f34c325d 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 @@ class Set 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 @@ class Set // 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 cb2d48dd3..926ffc9cf 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 51d2ded1a..704946854 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 ea0acd2b5..6e1fea6aa 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 1b97bb892..5fe9e787d 100644 --- a/Analysis/src/Clone.cpp +++ b/Analysis/src/Clone.cpp @@ -261,6 +261,11 @@ class TypeCloner2 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 15e64c920..12e4e7dab 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 c056a150f..9707225db 100644 --- a/Analysis/src/ConstraintSolver.cpp +++ b/Analysis/src/ConstraintSolver.cpp @@ -992,6 +992,27 @@ bool ConstraintSolver::tryDispatch(const FunctionCallConstraint& c, NotNull std::optional { auto it = begin(t); auto endIt = end(t); @@ -1017,10 +1038,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); } @@ -1135,23 +1158,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()) { @@ -1161,8 +1175,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; } @@ -1437,32 +1451,57 @@ bool ConstraintSolver::tryDispatch(const UnpackConstraint& c, NotNull= srcPack.head.size()) break; + TypeId srcTy = follow(srcPack.head[i]); + TypeId resultTy = follow(*resultIter); - if (isBlocked(*destIter)) + if (resultTy) { - if (follow(srcTy) == *destIter) + if (auto lt = getMutable(resultTy); c.resultIsLValue && lt) + { + 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)) { - // Cyclic type dependency. (????) - TypeId f = freshType(arena, builtinTypes, constraint->scope); - asMutable(*destIter)->ty.emplace(f); + 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; } @@ -1470,15 +1509,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; @@ -1998,14 +2047,23 @@ std::pair, std::optional> ConstraintSolver::lookupTa } else if (auto ft = get(subjectType)) { - Scope* scope = ft->scope; + const TypeId upperBound = follow(ft->upperBound); + + if (get(upperBound)) + return lookupTableProp(upperBound, propName, suppressSimplification, seen); + + // TODO: The upper bound could be an intersection that contains suitable tables or classes. - TableType* tt = &asMutable(subjectType)->ty.emplace(); - tt->state = TableState::Free; - tt->scope = scope; + 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)) @@ -2297,7 +2355,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 60d959867..bdefd7f0c 100644 --- a/Analysis/src/DataFlowGraph.cpp +++ b/Analysis/src/DataFlowGraph.cpp @@ -161,23 +161,107 @@ 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) @@ -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 fdbc089f3..2b3bbeac3 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 #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 5ff782ea3..451fa8f68 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 9f14b355e..3c9610473 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 6beffc2cd..a3182c0a5 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 176f15069..2f4e96112 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 f45f6d3e4..7e7c8cd62 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 09851024b..c42417119 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 cc01d626f..4f7869d02 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 fb47471c2..0e2462040 100644 --- a/Analysis/src/TypeAttach.cpp +++ b/Analysis/src/TypeAttach.cpp @@ -329,10 +329,14 @@ class TypeRehydrationVisitor { 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 8df78140f..0250817cf 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 000000000..5002ce1d1 --- /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 0dbfbee14..9f906fb2d 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 000000000..cfdd5f84f --- /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 22978dd4a..ec53916f4 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 000000000..7bf38cc4e --- /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 42450d3c0..6a1733a53 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 f7572a6cd..74a5bfd68 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); + } + 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); + build.mov(word[bufferAddrOp(inst.a, inst.b)], value); + } break; } diff --git a/CodeGen/src/IrTranslation.cpp b/CodeGen/src/IrTranslation.cpp index dff7002d4..91e87fdb5 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 8d0f829a9..45583fcc4 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 067a9d7a6..f175b169a 100644 --- a/Common/include/Luau/DenseHash.h +++ b/Common/include/Luau/DenseHash.h @@ -540,7 +540,7 @@ class DenseHashSet return impl.end(); } - bool operator==(const DenseHashSet& other) + bool operator==(const DenseHashSet& other) const { if (size() != other.size()) return false; @@ -554,7 +554,7 @@ class DenseHashSet return true; } - bool operator!=(const DenseHashSet& other) + bool operator!=(const DenseHashSet& other) const { return !(*this == other); } diff --git a/Makefile b/Makefile index 9e97633f6..4d606bef7 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 929c99a4d..eee66b863 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 968a55be8..db4941ee6 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 e957316e1..be0337a20 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 8f40f5741..b3a46e493 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 8eb778c84..e65635bd4 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 4476452a8..a70f4f61b 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 d7120bcac..cda1fbf5a 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 00c3c737f..be9fc75de 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 199b1b229..598e2dd92 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 1e3db8204..5d6b9b160 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 a6d3fa5cb..b53f60f0c 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 1fbd90082..bff86b034 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 6892c78f7..2023a838a 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 3c126c7ee..31f5f7f26 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 cba1f37e5..f0c70261e 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 1bc6b380a..6fda6a2a0 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 8e32a6a7c..d3c157289 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 5af349308..c34be936a 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 cee368329..2f07c1483 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 1cf996da5..5da2a688c 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 08d458f9f..63c6ff098 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 c421f2ce5..61fb4f474 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 30f8a3106..3598b02cb 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 557e77a6760541beadd35afd8878af01d08ee686 Mon Sep 17 00:00:00 2001 From: Vighnesh Date: Fri, 1 Dec 2023 18:04:44 -0800 Subject: [PATCH 004/116] VM - Add SUBRK and DIVRK bytecode instructions - Enables future performance optimizations Miscellaneous - Small performance improvements to new non-strict mode - Introduce more scripts for fuzzing - Improcements to dataflow analysis --- 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 | 57 +- Analysis/src/Error.cpp | 15 + Analysis/src/Frontend.cpp | 6 + Analysis/src/Instantiation.cpp | 2 + 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 | 45 +- CLI/Compile.cpp | 64 +- CLI/FileUtils.cpp | 161 ++++ CLI/FileUtils.h | 9 + CLI/Repl.cpp | 161 +++- CLI/Require.cpp | 290 ++++++ CLI/Require.h | 62 ++ CMakeLists.txt | 6 +- CMakePresets.json | 47 + CodeGen/include/Luau/BytecodeAnalysis.h | 21 + CodeGen/include/Luau/BytecodeSummary.h | 2 + CodeGen/include/Luau/CodeGen.h | 15 + CodeGen/include/Luau/IrBuilder.h | 8 +- CodeGen/include/Luau/IrData.h | 65 +- CodeGen/include/Luau/IrDump.h | 1 + CodeGen/include/Luau/IrVisitUseDef.h | 5 + CodeGen/src/BytecodeAnalysis.cpp | 884 ++++++++++++++++++ CodeGen/src/CodeGenAssembly.cpp | 34 +- CodeGen/src/CodeGenLower.h | 25 +- CodeGen/src/CodeGenUtils.cpp | 2 +- CodeGen/src/EmitCommonX64.cpp | 4 +- CodeGen/src/EmitCommonX64.h | 2 +- CodeGen/src/IrAnalysis.cpp | 1 + CodeGen/src/IrBuilder.cpp | 19 +- CodeGen/src/IrDump.cpp | 46 +- CodeGen/src/IrLoweringA64.cpp | 6 +- CodeGen/src/IrLoweringX64.cpp | 11 +- CodeGen/src/IrTranslateBuiltins.cpp | 31 +- CodeGen/src/IrTranslation.cpp | 221 +++-- CodeGen/src/IrTranslation.h | 3 +- CodeGen/src/OptimizeConstProp.cpp | 126 ++- Common/include/Luau/Bytecode.h | 17 +- Compiler/include/Luau/BytecodeBuilder.h | 9 +- Compiler/src/BuiltinFolding.cpp | 26 + Compiler/src/BytecodeBuilder.cpp | 110 ++- Compiler/src/Compiler.cpp | 67 +- Compiler/src/ConstantFolding.cpp | 4 + Compiler/src/ConstantFolding.h | 2 + Config/include/Luau/Config.h | 8 +- Config/src/Config.cpp | 45 + Makefile | 6 +- Sources.cmake | 9 +- VM/src/lvmexecute.cpp | 87 +- VM/src/lvmload.cpp | 11 + 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 | 161 +++- 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 | 256 ++++- tests/Normalize.test.cpp | 13 +- tests/Parser.test.cpp | 47 +- tests/RequireByString.test.cpp | 391 ++++++++ tests/RuntimeLimits.test.cpp | 9 +- 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 | 25 + 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 | 62 +- tools/fuzz/templates/index.html | 2 + 140 files changed, 4309 insertions(+), 1040 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 a026fdae6..bba3fcedc 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 69b0fd203..a3e1092f3 100644 --- a/Analysis/include/Luau/ConstraintGenerator.h +++ b/Analysis/include/Luau/ConstraintGenerator.h @@ -225,6 +225,7 @@ struct ConstraintGenerator 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 ddbf6dcb5..06ea96016 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 926ffc9cf..d2619fe27 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 49a275d51..4930df6fd 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 12e4e7dab..cdfe9a7f4 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 9707225db..fa0f767bc 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); @@ -1262,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) { @@ -1300,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)) { @@ -1327,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; } @@ -1336,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) @@ -1907,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; } @@ -1948,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 3be63f02a..5ec2d52b1 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/Frontend.cpp b/Analysis/src/Frontend.cpp index 710e36993..125f24579 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/Analysis/src/Instantiation.cpp b/Analysis/src/Instantiation.cpp index e74ece06e..235786a8a 100644 --- a/Analysis/src/Instantiation.cpp +++ b/Analysis/src/Instantiation.cpp @@ -7,6 +7,8 @@ #include "Luau/TypeArena.h" #include "Luau/TypeCheckLimits.h" +#include + LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution) namespace Luau diff --git a/Analysis/src/IostreamHelpers.cpp b/Analysis/src/IostreamHelpers.cpp index 7a58a2444..85a03b488 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 451fa8f68..2372a9a7a 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 3c9610473..30ab78959 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 7e7c8cd62..49db9cd31 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 4f7869d02..918da330e 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 0250817cf..32b91637a 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 9b470a2a2..fb4d68cbc 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; + + // 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); - return traverse(property); + 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 41a5afb08..6b213aea7 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 ad5592f50..2abda7883 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 510e5f0e1..e4b840b2e 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)) - { - char repr[512]; - snprintf(repr, sizeof(repr), "%.0f", value); + // trailing non-numeric characters + if (*end != 0) + return ConstantNumberParseResult::Malformed; - if (strcmp(repr, data) != 0) - return ConstantNumberParseResult::Imprecise; - } + result = value; - return ConstantNumberParseResult::Ok; - } - else + // 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)) { - result = value; - return *end == 0 ? ConstantNumberParseResult::Ok : ConstantNumberParseResult::Malformed; + char repr[512]; + snprintf(repr, sizeof(repr), "%.0f", value); + + if (strcmp(repr, data) != 0) + return ConstantNumberParseResult::Imprecise; } + + return ConstantNumberParseResult::Ok; } // simpleexp -> NUMBER | STRING | NIL | true | false | ... | constructor | FUNCTION body | primaryexp diff --git a/CLI/Compile.cpp b/CLI/Compile.cpp index bc1d5c602..950432717 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=