From 74c532053f829d31bb000c16d3c12732e37dbf3f Mon Sep 17 00:00:00 2001 From: Andy Friesen Date: Fri, 17 Nov 2023 10:46:18 -0800 Subject: [PATCH] Sync to upstream/release/604 (#1106) New Solver * New algorithm for inferring the types of locals that have no annotations. This algorithm is very conservative by default, but is augmented with some control flow awareness to handle most common scenarios. * Fix bugs in type inference of tables * Improve performance of by switching out standard C++ containers for `DenseHashMap` * Infrastructure to support clearer error messages in strict mode Native Code Generation * Fix a lowering issue with buffer.writeu8 and 0x80-0xff values: A constant argument wasn't truncated to the target type range and that causes an assertion failure in `build.mov`. * Store full lightuserdata value in loop iteration protocol lowering * Add analysis to compute function bytecode distribution * This includes a class to analyze the bytecode operator distribution per function and a CLI tool that produces a JSON report. See the new cmake target `Luau.Bytecode.CLI` --------- Co-authored-by: Aaron Weiss Co-authored-by: Alexander McCord Co-authored-by: Andy Friesen Co-authored-by: Aviral Goel Co-authored-by: Lily Brown Co-authored-by: Vyacheslav Egorov --- Analysis/include/Luau/Constraint.h | 5 + Analysis/include/Luau/ConstraintGenerator.h | 18 +- Analysis/include/Luau/DataFlowGraph.h | 15 +- Analysis/include/Luau/Def.h | 1 + Analysis/include/Luau/Scope.h | 1 + Analysis/include/Luau/Set.h | 68 +++- Analysis/include/Luau/Subtyping.h | 13 +- Analysis/include/Luau/Type.h | 20 +- Analysis/include/Luau/VisitType.h | 9 + Analysis/src/Clone.cpp | 11 + Analysis/src/ConstraintGenerator.cpp | 139 ++++----- Analysis/src/ConstraintSolver.cpp | 143 ++++++--- Analysis/src/DataFlowGraph.cpp | 137 ++++++-- Analysis/src/Def.cpp | 58 ++-- Analysis/src/NonStrictTypeChecker.cpp | 32 +- Analysis/src/Normalize.cpp | 6 + Analysis/src/Scope.cpp | 11 + Analysis/src/Substitution.cpp | 4 + Analysis/src/Subtyping.cpp | 23 +- Analysis/src/ToDot.cpp | 8 + Analysis/src/ToString.cpp | 19 ++ Analysis/src/TypeAttach.cpp | 6 +- Analysis/src/TypeChecker2.cpp | 11 +- CLI/Bytecode.cpp | 295 ++++++++++++++++++ CMakeLists.txt | 5 + CodeGen/include/Luau/BytecodeSummary.h | 81 +++++ CodeGen/src/AssemblyBuilderX64.cpp | 14 +- CodeGen/src/BytecodeSummary.cpp | 71 +++++ CodeGen/src/IrLoweringA64.cpp | 10 +- CodeGen/src/IrLoweringX64.cpp | 47 ++- CodeGen/src/IrTranslation.cpp | 5 +- CodeGen/src/OptimizeConstProp.cpp | 17 +- Common/include/Luau/DenseHash.h | 4 +- Makefile | 15 +- Sources.cmake | 12 + tests/Conformance.test.cpp | 69 ++++ tests/DataFlowGraph.test.cpp | 93 ++++++ tests/Fixture.cpp | 2 +- tests/NonStrictTypeChecker.test.cpp | 169 +++++++++- tests/Set.test.cpp | 39 +++ tests/Subtyping.test.cpp | 60 ++-- tests/ToString.test.cpp | 2 +- tests/TypeInfer.aliases.test.cpp | 6 +- tests/TypeInfer.anyerror.test.cpp | 10 +- tests/TypeInfer.classes.test.cpp | 54 ++-- tests/TypeInfer.generics.test.cpp | 11 +- tests/TypeInfer.intersectionTypes.test.cpp | 2 +- tests/TypeInfer.modules.test.cpp | 24 +- tests/TypeInfer.refinements.test.cpp | 13 +- tests/TypeInfer.singletons.test.cpp | 4 +- tests/TypeInfer.tables.test.cpp | 30 +- tests/TypeInfer.test.cpp | 85 ++--- tests/TypeInfer.typestates.test.cpp | 41 +++ tests/conformance/buffers.lua | 9 + tests/conformance/native.lua | 18 ++ tools/faillist.txt | 23 +- .../heuristicstat.py | 0 tools/test_dcr.py | 11 + 58 files changed, 1736 insertions(+), 373 deletions(-) create mode 100644 CLI/Bytecode.cpp create mode 100644 CodeGen/include/Luau/BytecodeSummary.h create mode 100644 CodeGen/src/BytecodeSummary.cpp rename stats/compiler-stats.py => tools/heuristicstat.py (100%) diff --git a/Analysis/include/Luau/Constraint.h b/Analysis/include/Luau/Constraint.h index ad10ca99..a026fdae 100644 --- a/Analysis/include/Luau/Constraint.h +++ b/Analysis/include/Luau/Constraint.h @@ -190,6 +190,11 @@ struct UnpackConstraint { TypePackId resultPack; TypePackId sourcePack; + + // UnpackConstraint is sometimes used to resolve the types of assignments. + // When this is the case, any LocalTypes in resultPack can have their + // domains extended by the corresponding type from sourcePack. + bool resultIsLValue = false; }; // resultType ~ refine type mode discriminant diff --git a/Analysis/include/Luau/ConstraintGenerator.h b/Analysis/include/Luau/ConstraintGenerator.h index aab31c40..69b0fd20 100644 --- a/Analysis/include/Luau/ConstraintGenerator.h +++ b/Analysis/include/Luau/ConstraintGenerator.h @@ -78,11 +78,13 @@ struct ConstraintGenerator TypeIds types; }; - // During constraint generation, we only populate the Scope::bindings - // property for annotated symbols. Unannotated symbols must be handled in a - // postprocessing step because we have not yet allocated the types that will - // be assigned to those unannotated symbols, so we queue them up here. - std::map inferredBindings; + // Some locals have multiple type states. We wish for Scope::bindings to + // map each local name onto the union of every type that the local can have + // over its lifetime, so we use this map to accumulate the set of types it + // might have. + // + // See the functions recordInferredBinding and fillInInferredBindings. + DenseHashMap inferredBindings{{}}; // Constraints that go straight to the solver. std::vector constraints; @@ -245,8 +247,6 @@ private: std::optional checkLValue(const ScopePtr& scope, AstExprIndexExpr* indexExpr, TypeId assignedTy); TypeId updateProperty(const ScopePtr& scope, AstExpr* expr, TypeId assignedTy); - void updateLValueType(AstExpr* lvalue, TypeId ty); - struct FunctionSignature { // The type of the function. @@ -336,6 +336,10 @@ private: */ void prepopulateGlobalScope(const ScopePtr& globalScope, AstStatBlock* program); + // Record the fact that a particular local has a particular type in at least + // one of its states. + void recordInferredBinding(AstLocal* local, TypeId ty); + void fillInInferredBindings(const ScopePtr& globalScope, AstStatBlock* block); /** Given a function type annotation, return a vector describing the expected types of the calls to the function diff --git a/Analysis/include/Luau/DataFlowGraph.h b/Analysis/include/Luau/DataFlowGraph.h index ab957b89..083e5046 100644 --- a/Analysis/include/Luau/DataFlowGraph.h +++ b/Analysis/include/Luau/DataFlowGraph.h @@ -77,8 +77,11 @@ struct DfgScope DfgScope* parent; bool isLoopScope; - DenseHashMap bindings{Symbol{}}; - DenseHashMap> props{nullptr}; + using Bindings = DenseHashMap; + using Props = DenseHashMap>; + + Bindings bindings{Symbol{}}; + Props props{nullptr}; std::optional lookup(Symbol symbol) const; std::optional lookup(DefId def, const std::string& key) const; @@ -115,7 +118,13 @@ private: std::vector> scopes; DfgScope* childScope(DfgScope* scope, bool isLoopScope = false); - void join(DfgScope* parent, DfgScope* a, DfgScope* b); + + void join(DfgScope* p, DfgScope* a, DfgScope* b); + void joinBindings(DfgScope::Bindings& p, const DfgScope::Bindings& a, const DfgScope::Bindings& b); + void joinProps(DfgScope::Props& p, const DfgScope::Props& a, const DfgScope::Props& b); + + DefId lookup(DfgScope* scope, Symbol symbol); + DefId lookup(DfgScope* scope, DefId def, const std::string& key); ControlFlow visit(DfgScope* scope, AstStatBlock* b); ControlFlow visitBlockWithoutChildScope(DfgScope* scope, AstStatBlock* b); diff --git a/Analysis/include/Luau/Def.h b/Analysis/include/Luau/Def.h index 0a85fdee..e3fec9b6 100644 --- a/Analysis/include/Luau/Def.h +++ b/Analysis/include/Luau/Def.h @@ -80,6 +80,7 @@ struct DefArena DefId freshCell(bool subscripted = false); DefId phi(DefId a, DefId b); + DefId phi(const std::vector& defs); }; } // namespace Luau diff --git a/Analysis/include/Luau/Scope.h b/Analysis/include/Luau/Scope.h index 2360c986..3f2b7355 100644 --- a/Analysis/include/Luau/Scope.h +++ b/Analysis/include/Luau/Scope.h @@ -56,6 +56,7 @@ struct Scope void addBuiltinTypeBinding(const Name& name, const TypeFun& tyFun); std::optional lookup(Symbol sym) const; + std::optional lookupUnrefinedType(DefId def) const; std::optional lookup(DefId def) const; std::optional> lookupEx(DefId def); std::optional> lookupEx(Symbol sym); diff --git a/Analysis/include/Luau/Set.h b/Analysis/include/Luau/Set.h index 5baff136..3f34c325 100644 --- a/Analysis/include/Luau/Set.h +++ b/Analysis/include/Luau/Set.h @@ -15,10 +15,14 @@ template> class Set { private: - DenseHashMap mapping; + using Impl = DenseHashMap; + Impl mapping; size_t entryCount = 0; public: + class const_iterator; + using iterator = const_iterator; + Set(const T& empty_key) : mapping{empty_key} { @@ -83,6 +87,16 @@ public: return count(element) != 0; } + const_iterator begin() const + { + return const_iterator(mapping.begin(), mapping.end()); + } + + const_iterator end() const + { + return const_iterator(mapping.end(), mapping.end()); + } + bool operator==(const Set& there) const { // if the sets are unequal sizes, then they cannot possibly be equal. @@ -100,6 +114,58 @@ public: // otherwise, we've proven the two equal! return true; } + + class const_iterator + { + public: + const_iterator(typename Impl::const_iterator impl, typename Impl::const_iterator end) + : impl(impl) + , end(end) + {} + + const T& operator*() const + { + return impl->first; + } + + const T* operator->() const + { + return &impl->first; + } + + + bool operator==(const const_iterator& other) const + { + return impl == other.impl; + } + + bool operator!=(const const_iterator& other) const + { + return impl != other.impl; + } + + + const_iterator& operator++() + { + do + { + impl++; + } while (impl != end && impl->second == false); + // keep iterating past pairs where the value is `false` + + return *this; + } + + const_iterator operator++(int) + { + const_iterator res = *this; + ++*this; + return res; + } + private: + typename Impl::const_iterator impl; + typename Impl::const_iterator end; + }; }; } // namespace Luau diff --git a/Analysis/include/Luau/Subtyping.h b/Analysis/include/Luau/Subtyping.h index cb2d48dd..926ffc9c 100644 --- a/Analysis/include/Luau/Subtyping.h +++ b/Analysis/include/Luau/Subtyping.h @@ -27,10 +27,19 @@ struct TypeArena; struct Scope; struct TableIndexer; +enum class SubtypingVariance +{ + // Used for an empty key. Should never appear in actual code. + Invalid, + Covariant, + Invariant, +}; + struct SubtypingReasoning { Path subPath; Path superPath; + SubtypingVariance variance = SubtypingVariance::Covariant; bool operator==(const SubtypingReasoning& other) const; }; @@ -49,7 +58,8 @@ struct SubtypingResult /// The reason for isSubtype to be false. May not be present even if /// isSubtype is false, depending on the input types. - DenseHashSet reasoning{SubtypingReasoning{}}; + DenseHashSet reasoning{ + SubtypingReasoning{TypePath::kEmpty, TypePath::kEmpty, SubtypingVariance::Invalid}}; SubtypingResult& andAlso(const SubtypingResult& other); SubtypingResult& orElse(const SubtypingResult& other); @@ -59,6 +69,7 @@ struct SubtypingResult SubtypingResult& withBothPath(TypePath::Path path); SubtypingResult& withSubPath(TypePath::Path path); SubtypingResult& withSuperPath(TypePath::Path path); + SubtypingResult& withVariance(SubtypingVariance variance); // Only negates the `isSubtype`. static SubtypingResult negate(const SubtypingResult& result); diff --git a/Analysis/include/Luau/Type.h b/Analysis/include/Luau/Type.h index 51d2ded1..70494685 100644 --- a/Analysis/include/Luau/Type.h +++ b/Analysis/include/Luau/Type.h @@ -86,6 +86,24 @@ struct FreeType TypeId upperBound = nullptr; }; +/** A type that tracks the domain of a local variable. + * + * We consider each local's domain to be the union of all types assigned to it. + * We accomplish this with LocalType. Each time we dispatch an assignment to a + * local, we accumulate this union and decrement blockCount. + * + * When blockCount reaches 0, we can consider the LocalType to be "fully baked" + * and replace it with the union we've built. + */ +struct LocalType +{ + TypeId domain; + int blockCount = 0; + + // Used for debugging + std::string name; +}; + struct GenericType { // By default, generics are global, with a synthetic name @@ -623,7 +641,7 @@ struct NegationType using ErrorType = Unifiable::Error; using TypeVariant = - Unifiable::Variant; struct Type final diff --git a/Analysis/include/Luau/VisitType.h b/Analysis/include/Luau/VisitType.h index ea0acd2b..6e1fea6a 100644 --- a/Analysis/include/Luau/VisitType.h +++ b/Analysis/include/Luau/VisitType.h @@ -97,6 +97,10 @@ struct GenericTypeVisitor { return visit(ty); } + virtual bool visit(TypeId ty, const LocalType& ftv) + { + return visit(ty); + } virtual bool visit(TypeId ty, const GenericType& gtv) { return visit(ty); @@ -241,6 +245,11 @@ struct GenericTypeVisitor else visit(ty, *ftv); } + else if (auto lt = get(ty)) + { + if (visit(ty, *lt)) + traverse(lt->domain); + } else if (auto gtv = get(ty)) visit(ty, *gtv); else if (auto etv = get(ty)) diff --git a/Analysis/src/Clone.cpp b/Analysis/src/Clone.cpp index 1b97bb89..5fe9e787 100644 --- a/Analysis/src/Clone.cpp +++ b/Analysis/src/Clone.cpp @@ -261,6 +261,11 @@ private: t->upperBound = shallowClone(t->upperBound); } + void cloneChildren(LocalType* t) + { + t->domain = shallowClone(t->domain); + } + void cloneChildren(GenericType* t) { // TOOD: clone upper bounds. @@ -504,6 +509,7 @@ struct TypeCloner void defaultClone(const T& t); void operator()(const FreeType& t); + void operator()(const LocalType& t); void operator()(const GenericType& t); void operator()(const BoundType& t); void operator()(const ErrorType& t); @@ -631,6 +637,11 @@ void TypeCloner::operator()(const FreeType& t) defaultClone(t); } +void TypeCloner::operator()(const LocalType& t) +{ + defaultClone(t); +} + void TypeCloner::operator()(const GenericType& t) { defaultClone(t); diff --git a/Analysis/src/ConstraintGenerator.cpp b/Analysis/src/ConstraintGenerator.cpp index 15e64c92..12e4e7da 100644 --- a/Analysis/src/ConstraintGenerator.cpp +++ b/Analysis/src/ConstraintGenerator.cpp @@ -205,33 +205,6 @@ ScopePtr ConstraintGenerator::childScope(AstNode* node, const ScopePtr& parent) return scope; } -static std::vector flatten(const Phi* phi) -{ - std::vector result; - - std::deque queue{phi->operands.begin(), phi->operands.end()}; - DenseHashSet seen{nullptr}; - - while (!queue.empty()) - { - DefId next = queue.front(); - queue.pop_front(); - - // Phi nodes should never be cyclic. - LUAU_ASSERT(!seen.find(next)); - if (seen.find(next)) - continue; - seen.insert(next); - - if (get(next)) - result.push_back(next); - else if (auto phi = get(next)) - queue.insert(queue.end(), phi->operands.begin(), phi->operands.end()); - } - - return result; -} - std::optional ConstraintGenerator::lookup(Scope* scope, DefId def) { if (get(def)) @@ -243,7 +216,7 @@ std::optional ConstraintGenerator::lookup(Scope* scope, DefId def) TypeId res = builtinTypes->neverType; - for (DefId operand : flatten(phi)) + for (DefId operand : phi->operands) { // `scope->lookup(operand)` may return nothing because it could be a phi node of globals, but one of // the operand of that global has never been assigned a type, and so it should be an error. @@ -621,8 +594,12 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStat* stat) ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatLocal* statLocal) { - std::vector> varTypes; - varTypes.reserve(statLocal->vars.size); + std::vector annotatedTypes; + annotatedTypes.reserve(statLocal->vars.size); + bool hasAnnotation = false; + + std::vector> expectedTypes; + expectedTypes.reserve(statLocal->vars.size); std::vector assignees; assignees.reserve(statLocal->vars.size); @@ -635,7 +612,8 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatLocal* stat { const Location location = local->location; - TypeId assignee = arena->addType(BlockedType{}); + TypeId assignee = arena->addType(LocalType{builtinTypes->neverType, /* blockCount */ 1, local->name.value}); + assignees.push_back(assignee); if (!firstValueType) @@ -643,16 +621,21 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatLocal* stat if (local->annotation) { + hasAnnotation = true; TypeId annotationTy = resolveType(scope, local->annotation, /* inTypeArguments */ false); - varTypes.push_back(annotationTy); - - addConstraint(scope, local->location, SubtypeConstraint{assignee, annotationTy}); + annotatedTypes.push_back(annotationTy); + expectedTypes.push_back(annotationTy); scope->bindings[local] = Binding{annotationTy, location}; } else { - varTypes.push_back(std::nullopt); + // annotatedTypes must contain one type per local. If a particular + // local has no annotation at, assume the most conservative thing. + annotatedTypes.push_back(builtinTypes->unknownType); + + expectedTypes.push_back(std::nullopt); + scope->bindings[local] = Binding{builtinTypes->unknownType, location}; inferredBindings[local] = {scope.get(), location, {assignee}}; } @@ -661,8 +644,12 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatLocal* stat scope->lvalueTypes[def] = assignee; } - TypePackId resultPack = checkPack(scope, statLocal->values, varTypes).tp; - addConstraint(scope, statLocal->location, UnpackConstraint{arena->addTypePack(std::move(assignees)), resultPack}); + TypePackId resultPack = checkPack(scope, statLocal->values, expectedTypes).tp; + addConstraint(scope, statLocal->location, UnpackConstraint{arena->addTypePack(std::move(assignees)), resultPack, /*resultIsLValue*/ true}); + + // Types must flow between whatever annotations were provided and the rhs expression. + if (hasAnnotation) + addConstraint(scope, statLocal->location, PackSubtypeConstraint{resultPack, arena->addTypePack(std::move(annotatedTypes))}); if (statLocal->vars.size == 1 && statLocal->values.size == 1 && firstValueType && scope.get() == rootScope) { @@ -1006,26 +993,22 @@ static void bindFreeType(TypeId a, TypeId b) ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatAssign* assign) { - std::vector> expectedTypes; - expectedTypes.reserve(assign->vars.size); - std::vector assignees; assignees.reserve(assign->vars.size); for (AstExpr* lvalue : assign->vars) { TypeId assignee = arena->addType(BlockedType{}); - assignees.push_back(assignee); checkLValue(scope, lvalue, assignee); + assignees.push_back(assignee); DefId def = dfg->getDef(lvalue); scope->lvalueTypes[def] = assignee; - updateLValueType(lvalue, assignee); } - TypePackId resultPack = checkPack(scope, assign->values, expectedTypes).tp; - addConstraint(scope, assign->location, UnpackConstraint{arena->addTypePack(std::move(assignees)), resultPack}); + TypePackId resultPack = checkPack(scope, assign->values).tp; + addConstraint(scope, assign->location, UnpackConstraint{arena->addTypePack(std::move(assignees)), resultPack, /*resultIsLValue*/ true}); return ControlFlow::None; } @@ -1545,8 +1528,7 @@ InferencePack ConstraintGenerator::checkPack(const ScopePtr& scope, AstExprCall* scope->lvalueTypes[def] = resultTy; // TODO: typestates: track this as an assignment scope->rvalueRefinements[def] = resultTy; // TODO: typestates: track this as an assignment - if (auto it = inferredBindings.find(targetLocal->local); it != inferredBindings.end()) - it->second.types.insert(resultTy); + recordInferredBinding(targetLocal->local, resultTy); } return InferencePack{arena->addTypePack({resultTy}), {refinementArena.variadic(returnRefinements)}}; @@ -1723,8 +1705,8 @@ Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprLocal* local) if (maybeTy) { TypeId ty = follow(*maybeTy); - if (auto it = inferredBindings.find(local->local); it != inferredBindings.end()) - it->second.types.insert(ty); + + recordInferredBinding(local->local, ty); return Inference{ty, refinementArena.proposition(key, builtinTypes->truthyType)}; } @@ -2210,23 +2192,35 @@ std::optional ConstraintGenerator::checkLValue(const ScopePtr& scope, As std::optional ConstraintGenerator::checkLValue(const ScopePtr& scope, AstExprLocal* local, TypeId assignedTy) { - /* - * The caller of this method uses the returned type to emit the proper - * SubtypeConstraint. - * - * At this point during constraint generation, the binding table is only - * populated by symbols that have type annotations. - * - * If this local has an interesting type annotation, it is important that we - * return that and constrain the assigned type. - */ std::optional annotatedTy = scope->lookup(local->local); + LUAU_ASSERT(annotatedTy); if (annotatedTy) addConstraint(scope, local->location, SubtypeConstraint{assignedTy, *annotatedTy}); - else if (auto it = inferredBindings.find(local->local); it == inferredBindings.end()) - ice->ice("Cannot find AstLocal* in either Scope::bindings or inferredBindings?"); - return annotatedTy; + const DefId defId = dfg->getDef(local); + std::optional ty = scope->lookupUnrefinedType(defId); + + if (ty) + { + if (auto lt = getMutable(*ty)) + ++lt->blockCount; + } + else + { + ty = arena->addType(LocalType{builtinTypes->neverType, /* blockCount */ 1, local->local->name.value}); + + scope->lvalueTypes[defId] = *ty; + } + + addConstraint(scope, local->location, UnpackConstraint{ + arena->addTypePack({*ty}), + arena->addTypePack({assignedTy}), + /*resultIsLValue*/ true + }); + + recordInferredBinding(local->local, *ty); + + return ty; } std::optional ConstraintGenerator::checkLValue(const ScopePtr& scope, AstExprGlobal* global, TypeId assignedTy) @@ -2379,15 +2373,6 @@ TypeId ConstraintGenerator::updateProperty(const ScopePtr& scope, AstExpr* expr, return assignedTy; } -void ConstraintGenerator::updateLValueType(AstExpr* lvalue, TypeId ty) -{ - if (auto local = lvalue->as()) - { - if (auto it = inferredBindings.find(local->local); it != inferredBindings.end()) - it->second.types.insert(ty); - } -} - Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprTable* expr, std::optional expectedType) { const bool expectedTypeIsFree = expectedType && get(follow(*expectedType)); @@ -2611,13 +2596,7 @@ ConstraintGenerator::FunctionSignature ConstraintGenerator::checkFunctionSignatu argTypes.push_back(argTy); argNames.emplace_back(FunctionArgument{local->name.value, local->location}); - if (local->annotation) - signatureScope->bindings[local] = Binding{argTy, local->location}; - else - { - signatureScope->bindings[local] = Binding{builtinTypes->neverType, local->location}; - inferredBindings[local] = {signatureScope.get(), {}}; - } + signatureScope->bindings[local] = Binding{argTy, local->location}; DefId def = dfg->getDef(local); signatureScope->lvalueTypes[def] = argTy; @@ -3125,6 +3104,12 @@ void ConstraintGenerator::prepopulateGlobalScope(const ScopePtr& globalScope, As program->visit(&gp); } +void ConstraintGenerator::recordInferredBinding(AstLocal* local, TypeId ty) +{ + if (InferredBinding* ib = inferredBindings.find(local)) + ib->types.insert(ty); +} + void ConstraintGenerator::fillInInferredBindings(const ScopePtr& globalScope, AstStatBlock* block) { for (const auto& [symbol, p] : inferredBindings) diff --git a/Analysis/src/ConstraintSolver.cpp b/Analysis/src/ConstraintSolver.cpp index 482997c4..de2f566d 100644 --- a/Analysis/src/ConstraintSolver.cpp +++ b/Analysis/src/ConstraintSolver.cpp @@ -993,6 +993,27 @@ bool ConstraintSolver::tryDispatch(const FunctionCallConstraint& c, NotNull std::optional { auto it = begin(t); auto endIt = end(t); @@ -1018,10 +1039,12 @@ bool ConstraintSolver::tryDispatch(const FunctionCallConstraint& c, NotNull callMm = findMetatableEntry(builtinTypes, errors, fn, "__call", constraint->location)) { - auto [head, tail] = flatten(c.argsPack); - head.insert(head.begin(), fn); + argsHead.insert(argsHead.begin(), fn); - argsPack = arena->addTypePack(TypePack{std::move(head), tail}); + if (argsTail && isBlocked(*argsTail)) + return block(*argsTail, constraint); + + argsPack = arena->addTypePack(TypePack{std::move(argsHead), argsTail}); fn = follow(*callMm); asMutable(c.result)->ty.emplace(constraint->scope); } @@ -1136,23 +1159,14 @@ bool ConstraintSolver::tryDispatch(const PrimitiveTypeConstraint& c, NotNull constraint) { - TypeId subjectType = follow(c.subjectType); + const TypeId subjectType = follow(c.subjectType); + const TypeId resultType = follow(c.resultType); - LUAU_ASSERT(get(c.resultType)); + LUAU_ASSERT(get(resultType)); if (isBlocked(subjectType) || get(subjectType)) return block(subjectType, constraint); - if (get(subjectType)) - { - TableType& ttv = asMutable(subjectType)->ty.emplace(TableState::Free, TypeLevel{}, constraint->scope); - ttv.props[c.prop] = Property{c.resultType}; - TypeId res = freshType(arena, builtinTypes, constraint->scope); - asMutable(c.resultType)->ty.emplace(res); - unblock(c.resultType, constraint->location); - return true; - } - auto [blocked, result] = lookupTableProp(subjectType, c.prop, c.suppressSimplification); if (!blocked.empty()) { @@ -1162,8 +1176,8 @@ bool ConstraintSolver::tryDispatch(const HasPropConstraint& c, NotNullanyType), c.subjectType, constraint->location); - unblock(c.resultType, constraint->location); + bindBlockedType(resultType, result.value_or(builtinTypes->anyType), c.subjectType, constraint->location); + unblock(resultType, constraint->location); return true; } @@ -1438,32 +1452,57 @@ bool ConstraintSolver::tryDispatch(const UnpackConstraint& c, NotNull= srcPack.head.size()) break; - TypeId srcTy = follow(srcPack.head[i]); - if (isBlocked(*destIter)) + TypeId srcTy = follow(srcPack.head[i]); + TypeId resultTy = follow(*resultIter); + + if (resultTy) { - if (follow(srcTy) == *destIter) + if (auto lt = getMutable(resultTy); c.resultIsLValue && lt) { - // Cyclic type dependency. (????) - TypeId f = freshType(arena, builtinTypes, constraint->scope); - asMutable(*destIter)->ty.emplace(f); + lt->domain = simplifyUnion(builtinTypes, arena, lt->domain, srcTy).result; + LUAU_ASSERT(lt->blockCount > 0); + --lt->blockCount; + + LUAU_ASSERT(0 <= lt->blockCount); + + if (0 == lt->blockCount) + asMutable(resultTy)->ty.emplace(lt->domain); + } + else if (get(resultTy)) + { + if (follow(srcTy) == resultTy) + { + // It is sometimes the case that we find that a blocked type + // is only blocked on itself. This doesn't actually + // constitute any meaningful constraint, so we replace it + // with a free type. + TypeId f = freshType(arena, builtinTypes, constraint->scope); + asMutable(resultTy)->ty.emplace(f); + } + else + asMutable(resultTy)->ty.emplace(srcTy); } else - asMutable(*destIter)->ty.emplace(srcTy); - unblock(*destIter, constraint->location); + { + LUAU_ASSERT(c.resultIsLValue); + unify(constraint->scope, constraint->location, resultTy, srcTy); + } + + unblock(resultTy, constraint->location); } else - unify(constraint->scope, constraint->location, *destIter, srcTy); + unify(constraint->scope, constraint->location, resultTy, srcTy); - ++destIter; + ++resultIter; ++i; } @@ -1471,15 +1510,25 @@ bool ConstraintSolver::tryDispatch(const UnpackConstraint& c, NotNull(resultTy); c.resultIsLValue && lt) { - asMutable(*destIter)->ty.emplace(builtinTypes->nilType); - unblock(*destIter, constraint->location); + lt->domain = simplifyUnion(builtinTypes, arena, lt->domain, builtinTypes->nilType).result; + LUAU_ASSERT(0 <= lt->blockCount); + --lt->blockCount; + + if (0 == lt->blockCount) + asMutable(resultTy)->ty.emplace(lt->domain); + } + else if (get(*resultIter) || get(*resultIter)) + { + asMutable(*resultIter)->ty.emplace(builtinTypes->nilType); + unblock(*resultIter, constraint->location); } - ++destIter; + ++resultIter; } return true; @@ -1999,14 +2048,23 @@ std::pair, std::optional> ConstraintSolver::lookupTa } else if (auto ft = get(subjectType)) { - Scope* scope = ft->scope; + const TypeId upperBound = follow(ft->upperBound); - TableType* tt = &asMutable(subjectType)->ty.emplace(); - tt->state = TableState::Free; - tt->scope = scope; + if (get(upperBound)) + return lookupTableProp(upperBound, propName, suppressSimplification, seen); + + // TODO: The upper bound could be an intersection that contains suitable tables or classes. + + NotNull scope{ft->scope}; + + const TypeId newUpperBound = arena->addType(TableType{TableState::Free, TypeLevel{}, scope}); + TableType* tt = getMutable(newUpperBound); + LUAU_ASSERT(tt); TypeId propType = freshType(arena, builtinTypes, scope); tt->props[propName] = Property{propType}; + unify(scope, Location{}, subjectType, newUpperBound); + return {{}, propType}; } else if (auto utv = get(subjectType)) @@ -2298,7 +2356,12 @@ void ConstraintSolver::unblock(const std::vector& packs, Location lo bool ConstraintSolver::isBlocked(TypeId ty) { - return nullptr != get(follow(ty)) || nullptr != get(follow(ty)); + ty = follow(ty); + + if (auto lt = get(ty)) + return lt->blockCount > 0; + + return nullptr != get(ty) || nullptr != get(ty); } bool ConstraintSolver::isBlocked(TypePackId tp) diff --git a/Analysis/src/DataFlowGraph.cpp b/Analysis/src/DataFlowGraph.cpp index 60d95986..bdefd7f0 100644 --- a/Analysis/src/DataFlowGraph.cpp +++ b/Analysis/src/DataFlowGraph.cpp @@ -161,25 +161,109 @@ DfgScope* DataFlowGraphBuilder::childScope(DfgScope* scope, bool isLoopScope) void DataFlowGraphBuilder::join(DfgScope* p, DfgScope* a, DfgScope* b) { - // TODO TODO FIXME IMPLEMENT JOIN LOGIC FOR PROPERTIES + joinBindings(p->bindings, a->bindings, b->bindings); + joinProps(p->props, a->props, b->props); +} - for (const auto& [sym, def1] : a->bindings) +void DataFlowGraphBuilder::joinBindings(DfgScope::Bindings& p, const DfgScope::Bindings& a, const DfgScope::Bindings& b) +{ + for (const auto& [sym, def1] : a) { - if (auto def2 = b->bindings.find(sym)) - p->bindings[sym] = defArena->phi(NotNull{def1}, NotNull{*def2}); - else if (auto def2 = p->bindings.find(sym)) - p->bindings[sym] = defArena->phi(NotNull{def1}, NotNull{*def2}); + if (auto def2 = b.find(sym)) + p[sym] = defArena->phi(NotNull{def1}, NotNull{*def2}); + else if (auto def2 = p.find(sym)) + p[sym] = defArena->phi(NotNull{def1}, NotNull{*def2}); } - for (const auto& [sym, def1] : b->bindings) + for (const auto& [sym, def1] : b) { - if (a->bindings.find(sym)) + if (auto def2 = p.find(sym)) + p[sym] = defArena->phi(NotNull{def1}, NotNull{*def2}); + } +} + +void DataFlowGraphBuilder::joinProps(DfgScope::Props& p, const DfgScope::Props& a, const DfgScope::Props& b) +{ + auto phinodify = [this](auto& p, const auto& a, const auto& b) mutable { + for (const auto& [k, defA] : a) + { + if (auto it = b.find(k); it != b.end()) + p[k] = defArena->phi(NotNull{it->second}, NotNull{defA}); + else if (auto it = p.find(k); it != p.end()) + p[k] = defArena->phi(NotNull{it->second}, NotNull{defA}); + else + p[k] = defA; + } + + for (const auto& [k, defB] : b) + { + if (auto it = a.find(k); it != a.end()) + continue; + else if (auto it = p.find(k); it != p.end()) + p[k] = defArena->phi(NotNull{it->second}, NotNull{defB}); + else + p[k] = defB; + } + }; + + for (const auto& [def, a1] : a) + { + p.try_insert(def, {}); + if (auto a2 = b.find(def)) + phinodify(p[def], a1, *a2); + else if (auto a2 = p.find(def)) + phinodify(p[def], a1, *a2); + } + + for (const auto& [def, a1] : b) + { + p.try_insert(def, {}); + if (a.find(def)) continue; - else if (auto def2 = p->bindings.find(sym)) - p->bindings[sym] = defArena->phi(NotNull{def1}, NotNull{*def2}); + else if (auto a2 = p.find(def)) + phinodify(p[def], a1, *a2); } } +DefId DataFlowGraphBuilder::lookup(DfgScope* scope, Symbol symbol) +{ + if (auto found = scope->lookup(symbol)) + return *found; + else + { + DefId result = defArena->freshCell(); + if (symbol.local) + scope->bindings[symbol] = result; + else + moduleScope->bindings[symbol] = result; + return result; + } +} + +DefId DataFlowGraphBuilder::lookup(DfgScope* scope, DefId def, const std::string& key) +{ + if (auto found = scope->lookup(def, key)) + return *found; + else if (auto phi = get(def)) + { + std::vector defs; + for (DefId operand : phi->operands) + defs.push_back(lookup(scope, operand, key)); + + DefId result = defArena->phi(defs); + scope->props[def][key] = result; + return result; + } + else if (get(def)) + { + DefId result = defArena->freshCell(); + scope->props[def][key] = result; + return result; + } + else + handle->ice("Inexhaustive lookup cases in DataFlowGraphBuilder::lookup"); +} + ControlFlow DataFlowGraphBuilder::visit(DfgScope* scope, AstStatBlock* b) { DfgScope* child = childScope(scope); @@ -585,6 +669,7 @@ DataFlowResult DataFlowGraphBuilder::visitExpr(DfgScope* scope, AstExprGroup* gr DataFlowResult DataFlowGraphBuilder::visitExpr(DfgScope* scope, AstExprLocal* l) { + // DfgScope::lookup is intentional here: we want to be able to ice. if (auto def = scope->lookup(l->local)) { const RefinementKey* key = keyArena->leaf(*def); @@ -596,11 +681,7 @@ DataFlowResult DataFlowGraphBuilder::visitExpr(DfgScope* scope, AstExprLocal* l) DataFlowResult DataFlowGraphBuilder::visitExpr(DfgScope* scope, AstExprGlobal* g) { - if (auto def = scope->lookup(g->name)) - return {*def, keyArena->leaf(*def)}; - - DefId def = defArena->freshCell(); - moduleScope->bindings[g->name] = def; + DefId def = lookup(scope, g->name); return {def, keyArena->leaf(def)}; } @@ -619,14 +700,9 @@ DataFlowResult DataFlowGraphBuilder::visitExpr(DfgScope* scope, AstExprIndexName auto [parentDef, parentKey] = visitExpr(scope, i->expr); std::string index = i->index.value; - if (auto propDef = scope->lookup(parentDef, index)) - return {*propDef, keyArena->node(parentKey, *propDef, index)}; - else - { - DefId def = defArena->freshCell(); - scope->props[parentDef][index] = def; - return {def, keyArena->node(parentKey, def, index)}; - } + + DefId def = lookup(scope, parentDef, index); + return {def, keyArena->node(parentKey, def, index)}; } DataFlowResult DataFlowGraphBuilder::visitExpr(DfgScope* scope, AstExprIndexExpr* i) @@ -637,14 +713,9 @@ DataFlowResult DataFlowGraphBuilder::visitExpr(DfgScope* scope, AstExprIndexExpr if (auto string = i->index->as()) { std::string index{string->value.data, string->value.size}; - if (auto propDef = scope->lookup(parentDef, index)) - return {*propDef, keyArena->node(parentKey, *propDef, index)}; - else - { - DefId def = defArena->freshCell(); - scope->props[parentDef][index] = def; - return {def, keyArena->node(parentKey, def, index)}; - } + + DefId def = lookup(scope, parentDef, index); + return {def, keyArena->node(parentKey, def, index)}; } return {defArena->freshCell(/* subscripted= */true), nullptr}; @@ -795,8 +866,8 @@ void DataFlowGraphBuilder::visitLValue(DfgScope* scope, AstExprGlobal* g, DefId // We need to keep the previous def around for a compound assignment. if (isCompoundAssignment) { - if (auto def = scope->lookup(g->name)) - graph.compoundAssignDefs[g] = *def; + DefId def = lookup(scope, g->name); + graph.compoundAssignDefs[g] = def; } // In order to avoid alias tracking, we need to clip the reference to the parent def. diff --git a/Analysis/src/Def.cpp b/Analysis/src/Def.cpp index fdbc089f..2b3bbeac 100644 --- a/Analysis/src/Def.cpp +++ b/Analysis/src/Def.cpp @@ -1,8 +1,9 @@ // This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details #include "Luau/Def.h" -#include "Luau/Common.h" -#include "Luau/DenseHash.h" +#include "Luau/Common.h" + +#include #include namespace Luau @@ -13,27 +14,9 @@ bool containsSubscriptedDefinition(DefId def) if (auto cell = get(def)) return cell->subscripted; else if (auto phi = get(def)) - { - std::deque queue(begin(phi->operands), end(phi->operands)); - DenseHashSet seen{nullptr}; - - while (!queue.empty()) - { - DefId next = queue.front(); - queue.pop_front(); - - LUAU_ASSERT(!seen.find(next)); - if (seen.find(next)) - continue; - seen.insert(next); - - if (auto cell_ = get(next); cell_ && cell_->subscripted) - return true; - else if (auto phi_ = get(next)) - queue.insert(queue.end(), phi_->operands.begin(), phi_->operands.end()); - } - } - return false; + return std::any_of(phi->operands.begin(), phi->operands.end(), containsSubscriptedDefinition); + else + return false; } DefId DefArena::freshCell(bool subscripted) @@ -41,12 +24,35 @@ DefId DefArena::freshCell(bool subscripted) return NotNull{allocator.allocate(Def{Cell{subscripted}})}; } +static void collectOperands(DefId def, std::vector& operands) +{ + if (std::find(operands.begin(), operands.end(), def) != operands.end()) + return; + else if (get(def)) + operands.push_back(def); + else if (auto phi = get(def)) + { + for (const Def* operand : phi->operands) + collectOperands(NotNull{operand}, operands); + } +} + DefId DefArena::phi(DefId a, DefId b) { - if (a == b) - return a; + return phi({a, b}); +} + +DefId DefArena::phi(const std::vector& defs) +{ + std::vector operands; + for (DefId operand : defs) + collectOperands(operand, operands); + + // There's no need to allocate a Phi node for a singleton set. + if (operands.size() == 1) + return operands[0]; else - return NotNull{allocator.allocate(Def{Phi{{a, b}}})}; + return NotNull{allocator.allocate(Def{Phi{std::move(operands)}})}; } } // namespace Luau diff --git a/Analysis/src/NonStrictTypeChecker.cpp b/Analysis/src/NonStrictTypeChecker.cpp index 5ff782ea..451fa8f6 100644 --- a/Analysis/src/NonStrictTypeChecker.cpp +++ b/Analysis/src/NonStrictTypeChecker.cpp @@ -84,7 +84,7 @@ struct NonStrictContext for (auto [def, rightTy] : right.context) { - if (!right.find(def).has_value()) + if (!left.find(def).has_value()) disj.context[def] = rightTy; } @@ -270,18 +270,24 @@ struct NonStrictTypeChecker NonStrictContext visit(AstStatBlock* block) { auto StackPusher = pushStack(block); + NonStrictContext ctx; for (AstStat* statement : block->body) - visit(statement); - return {}; + ctx = NonStrictContext::disjunction(builtinTypes, NotNull{&arena}, ctx, visit(statement)); + return ctx; } NonStrictContext visit(AstStatIf* ifStatement) { NonStrictContext condB = visit(ifStatement->condition); - NonStrictContext thenB = visit(ifStatement->thenbody); - NonStrictContext elseB = visit(ifStatement->elsebody); - return NonStrictContext::disjunction( - builtinTypes, NotNull{&arena}, condB, NonStrictContext::conjunction(builtinTypes, NotNull{&arena}, thenB, elseB)); + NonStrictContext branchContext; + // If there is no else branch, don't bother generating warnings for the then branch - we can't prove there is an error + if (ifStatement->elsebody) + { + NonStrictContext thenBody = visit(ifStatement->thenbody); + NonStrictContext elseBody = visit(ifStatement->elsebody); + branchContext = NonStrictContext::conjunction(builtinTypes, NotNull{&arena}, thenBody, elseBody); + } + return NonStrictContext::disjunction(builtinTypes, NotNull{&arena}, condB, branchContext); } NonStrictContext visit(AstStatWhile* whileStatement) @@ -316,6 +322,8 @@ struct NonStrictTypeChecker NonStrictContext visit(AstStatLocal* local) { + for (AstExpr* rhs : local->values) + visit(rhs); return {}; } @@ -341,12 +349,12 @@ struct NonStrictTypeChecker NonStrictContext visit(AstStatFunction* statFn) { - return {}; + return visit(statFn->func); } NonStrictContext visit(AstStatLocalFunction* localFn) { - return {}; + return visit(localFn->func); } NonStrictContext visit(AstStatTypeAlias* typeAlias) @@ -530,7 +538,7 @@ struct NonStrictTypeChecker NonStrictContext visit(AstExprFunction* exprFn) { auto pusher = pushStack(exprFn); - return {}; + return visit(exprFn->body); } NonStrictContext visit(AstExprTable* table) @@ -589,10 +597,6 @@ struct NonStrictTypeChecker SubtypingResult r = subtyping.isSubtype(actualType, *contextTy); if (r.normalizationTooComplex) reportError(NormalizationTooComplex{}, fragment->location); - - if (!r.isSubtype && !r.isErrorSuppressing) - reportError(TypeMismatch{actualType, *contextTy}, fragment->location); - if (r.isSubtype) return {actualType}; } diff --git a/Analysis/src/Normalize.cpp b/Analysis/src/Normalize.cpp index 9f14b355..3c961047 100644 --- a/Analysis/src/Normalize.cpp +++ b/Analysis/src/Normalize.cpp @@ -1623,6 +1623,12 @@ bool Normalizer::unionNormalWithTy(NormalizedType& here, TypeId there, SetunknownType; here.tyvars.insert_or_assign(there, std::make_unique(std::move(inter))); } + else if (auto lt = get(there)) + { + // FIXME? This is somewhat questionable. + // Maybe we should assert because this should never happen? + unionNormalWithTy(here, lt->domain, seenSetTypes, ignoreSmallerTyvars); + } else if (get(there)) unionFunctionsWithFunction(here.functions, there); else if (get(there) || get(there)) diff --git a/Analysis/src/Scope.cpp b/Analysis/src/Scope.cpp index 6beffc2c..a3182c0a 100644 --- a/Analysis/src/Scope.cpp +++ b/Analysis/src/Scope.cpp @@ -72,6 +72,17 @@ std::optional> Scope::lookupEx(Symbol sym) } } +std::optional Scope::lookupUnrefinedType(DefId def) const +{ + for (const Scope* current = this; current; current = current->parent.get()) + { + if (auto ty = current->lvalueTypes.find(def)) + return *ty; + } + + return std::nullopt; +} + std::optional Scope::lookup(DefId def) const { for (const Scope* current = this; current; current = current->parent.get()) diff --git a/Analysis/src/Substitution.cpp b/Analysis/src/Substitution.cpp index 176f1506..2f4e9611 100644 --- a/Analysis/src/Substitution.cpp +++ b/Analysis/src/Substitution.cpp @@ -19,8 +19,12 @@ static TypeId shallowClone(TypeId ty, TypeArena& dest, const TxnLog* log, bool a auto go = [ty, &dest, alwaysClone](auto&& a) { using T = std::decay_t; + // The pointer identities of free and local types is very important. + // We decline to copy them. if constexpr (std::is_same_v) return ty; + else if constexpr (std::is_same_v) + return ty; else if constexpr (std::is_same_v) { // This should never happen, but visit() cannot see it. diff --git a/Analysis/src/Subtyping.cpp b/Analysis/src/Subtyping.cpp index f45f6d3e..7e7c8cd6 100644 --- a/Analysis/src/Subtyping.cpp +++ b/Analysis/src/Subtyping.cpp @@ -47,12 +47,12 @@ struct VarianceFlipper bool SubtypingReasoning::operator==(const SubtypingReasoning& other) const { - return subPath == other.subPath && superPath == other.superPath; + return subPath == other.subPath && superPath == other.superPath && variance == other.variance; } size_t SubtypingReasoningHash::operator()(const SubtypingReasoning& r) const { - return TypePath::PathHash()(r.subPath) ^ (TypePath::PathHash()(r.superPath) << 1); + return TypePath::PathHash()(r.subPath) ^ (TypePath::PathHash()(r.superPath) << 1) ^ (static_cast(r.variance) << 1); } SubtypingResult& SubtypingResult::andAlso(const SubtypingResult& other) @@ -162,6 +162,19 @@ SubtypingResult& SubtypingResult::withSuperPath(TypePath::Path path) return *this; } +SubtypingResult& SubtypingResult::withVariance(SubtypingVariance variance) +{ + if (reasoning.empty()) + reasoning.insert(SubtypingReasoning{TypePath::kEmpty, TypePath::kEmpty, variance}); + else + { + for (auto& r : reasoning) + r.variance = variance; + } + + return *this; +} + SubtypingResult SubtypingResult::negate(const SubtypingResult& result) { return SubtypingResult{ @@ -671,7 +684,7 @@ SubtypingResult Subtyping::isContravariantWith(SubtypingEnvironment& env, SubTy& template SubtypingResult Subtyping::isInvariantWith(SubtypingEnvironment& env, SubTy&& subTy, SuperTy&& superTy) { - return isCovariantWith(env, subTy, superTy).andAlso(isContravariantWith(env, subTy, superTy)); + return isCovariantWith(env, subTy, superTy).andAlso(isContravariantWith(env, subTy, superTy)).withVariance(SubtypingVariance::Invariant); } template @@ -689,7 +702,7 @@ SubtypingResult Subtyping::isContravariantWith(SubtypingEnvironment& env, const template SubtypingResult Subtyping::isInvariantWith(SubtypingEnvironment& env, const TryPair& pair) { - return isCovariantWith(env, pair).andAlso(isContravariantWith(pair)); + return isCovariantWith(env, pair).andAlso(isContravariantWith(pair)).withVariance(SubtypingVariance::Invariant); } /* @@ -1009,7 +1022,7 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const Meta SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const MetatableType* subMt, const TableType* superTable) { - if (auto subTable = get(subMt->table)) + if (auto subTable = get(follow(subMt->table))) { // Metatables cannot erase properties from the table they're attached to, so // the subtyping rule for this is just if the table component is a subtype diff --git a/Analysis/src/ToDot.cpp b/Analysis/src/ToDot.cpp index 09851024..c4241711 100644 --- a/Analysis/src/ToDot.cpp +++ b/Analysis/src/ToDot.cpp @@ -261,6 +261,14 @@ void StateDot::visitChildren(TypeId ty, int index) visitChild(t.upperBound, index, "[upperBound]"); } } + else if constexpr (std::is_same_v) + { + formatAppend(result, "LocalType"); + finishNodeLabel(ty); + finishNode(); + + visitChild(t.domain, 1, "[domain]"); + } else if constexpr (std::is_same_v) { formatAppend(result, "AnyType %d", index); diff --git a/Analysis/src/ToString.cpp b/Analysis/src/ToString.cpp index cc01d626..4f7869d0 100644 --- a/Analysis/src/ToString.cpp +++ b/Analysis/src/ToString.cpp @@ -100,6 +100,16 @@ struct FindCyclicTypes final : TypeVisitor return false; } + bool visit(TypeId ty, const LocalType& lt) override + { + if (!visited.insert(ty).second) + return false; + + traverse(lt.domain); + + return false; + } + bool visit(TypeId ty, const TableType& ttv) override { if (!visited.insert(ty).second) @@ -500,6 +510,15 @@ struct TypeStringifier } } + void operator()(TypeId ty, const LocalType& lt) + { + state.emit("l-"); + state.emit(lt.name); + state.emit("=["); + stringify(lt.domain); + state.emit("]"); + } + void operator()(TypeId, const BoundType& btv) { stringify(btv.boundTo); diff --git a/Analysis/src/TypeAttach.cpp b/Analysis/src/TypeAttach.cpp index fb47471c..0e246204 100644 --- a/Analysis/src/TypeAttach.cpp +++ b/Analysis/src/TypeAttach.cpp @@ -329,10 +329,14 @@ public: { return Luau::visit(*this, bound.boundTo->ty); } - AstType* operator()(const FreeType& ftv) + AstType* operator()(const FreeType& ft) { return allocator->alloc(Location(), std::nullopt, AstName("free"), std::nullopt, Location()); } + AstType* operator()(const LocalType& lt) + { + return Luau::visit(*this, lt.domain->ty); + } AstType* operator()(const UnionType& uv) { AstArray unionTypes; diff --git a/Analysis/src/TypeChecker2.cpp b/Analysis/src/TypeChecker2.cpp index 8df78140..0250817c 100644 --- a/Analysis/src/TypeChecker2.cpp +++ b/Analysis/src/TypeChecker2.cpp @@ -2464,13 +2464,16 @@ struct TypeChecker2 if (!get2(*subLeaf, *superLeaf) && !get2(*subLeaf, *superLeaf)) ice->ice("Subtyping test returned a reasoning where one path ends at a type and the other ends at a pack.", location); + std::string relation = "a subtype of"; + if (reasoning.variance == SubtypingVariance::Invariant) + relation = "exactly"; + std::string reason; if (reasoning.subPath == reasoning.superPath) - reason = "at " + toString(reasoning.subPath) + ", " + toString(*subLeaf) + " is not a subtype of " + toString(*superLeaf); + reason = "at " + toString(reasoning.subPath) + ", " + toString(*subLeaf) + " is not " + relation + " " + toString(*superLeaf); else - reason = "type " + toString(subTy) + toString(reasoning.subPath, /* prefixDot */ true) + " (" + toString(*subLeaf) + - ") is not a subtype of " + toString(superTy) + toString(reasoning.superPath, /* prefixDot */ true) + " (" + - toString(*superLeaf) + ")"; + reason = "type " + toString(subTy) + toString(reasoning.subPath, /* prefixDot */ true) + " (" + toString(*subLeaf) + ") is not " + + relation + " " + toString(superTy) + toString(reasoning.superPath, /* prefixDot */ true) + " (" + toString(*superLeaf) + ")"; reasons.push_back(reason); } diff --git a/CLI/Bytecode.cpp b/CLI/Bytecode.cpp new file mode 100644 index 00000000..5002ce1d --- /dev/null +++ b/CLI/Bytecode.cpp @@ -0,0 +1,295 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#include "lua.h" +#include "lualib.h" + +#include "Luau/CodeGen.h" +#include "Luau/Compiler.h" +#include "Luau/BytecodeBuilder.h" +#include "Luau/Parser.h" +#include "Luau/BytecodeSummary.h" +#include "FileUtils.h" +#include "Flags.h" + +#include + +using Luau::CodeGen::FunctionBytecodeSummary; + +struct GlobalOptions +{ + int optimizationLevel = 1; + int debugLevel = 1; +} globalOptions; + +static Luau::CompileOptions copts() +{ + Luau::CompileOptions result = {}; + result.optimizationLevel = globalOptions.optimizationLevel; + result.debugLevel = globalOptions.debugLevel; + + return result; +} + +static void displayHelp(const char* argv0) +{ + printf("Usage: %s [options] [file list]\n", argv0); + printf("\n"); + printf("Available options:\n"); + printf(" -h, --help: Display this usage message.\n"); + printf(" -O: compile with optimization level n (default 1, n should be between 0 and 2).\n"); + printf(" -g: compile with debug level n (default 1, n should be between 0 and 2).\n"); + printf(" --fflags=: flags to be enabled.\n"); + printf(" --summary-file=: file in which bytecode analysis summary will be recorded (default 'bytecode-summary.json').\n"); + + exit(0); +} + +static bool parseArgs(int argc, char** argv, std::string& summaryFile) +{ + for (int i = 1; i < argc; i++) + { + if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) + { + displayHelp(argv[0]); + } + else if (strncmp(argv[i], "-O", 2) == 0) + { + int level = atoi(argv[i] + 2); + if (level < 0 || level > 2) + { + fprintf(stderr, "Error: Optimization level must be between 0 and 2 inclusive.\n"); + return false; + } + globalOptions.optimizationLevel = level; + } + else if (strncmp(argv[i], "-g", 2) == 0) + { + int level = atoi(argv[i] + 2); + if (level < 0 || level > 2) + { + fprintf(stderr, "Error: Debug level must be between 0 and 2 inclusive.\n"); + return false; + } + globalOptions.debugLevel = level; + } + else if (strncmp(argv[i], "--summary-file=", 15) == 0) + { + summaryFile = argv[i] + 15; + + if (summaryFile.size() == 0) + { + fprintf(stderr, "Error: filename missing for '--summary-file'.\n\n"); + return false; + } + } + else if (strncmp(argv[i], "--fflags=", 9) == 0) + { + setLuauFlags(argv[i] + 9); + } + else if (argv[i][0] == '-') + { + fprintf(stderr, "Error: Unrecognized option '%s'.\n\n", argv[i]); + displayHelp(argv[0]); + } + } + + return true; +} + +static void report(const char* name, const Luau::Location& location, const char* type, const char* message) +{ + fprintf(stderr, "%s(%d,%d): %s: %s\n", name, location.begin.line + 1, location.begin.column + 1, type, message); +} + +static void reportError(const char* name, const Luau::ParseError& error) +{ + report(name, error.getLocation(), "SyntaxError", error.what()); +} + +static void reportError(const char* name, const Luau::CompileError& error) +{ + report(name, error.getLocation(), "CompileError", error.what()); +} + +static bool analyzeFile(const char* name, const unsigned nestingLimit, std::vector& summaries) +{ + std::optional source = readFile(name); + + if (!source) + { + fprintf(stderr, "Error opening %s\n", name); + return false; + } + + try + { + Luau::BytecodeBuilder bcb; + + compileOrThrow(bcb, source.value(), copts()); + + const std::string& bytecode = bcb.getBytecode(); + + std::unique_ptr globalState(luaL_newstate(), lua_close); + lua_State* L = globalState.get(); + + if (luau_load(L, name, bytecode.data(), bytecode.size(), 0) == 0) + { + summaries = Luau::CodeGen::summarizeBytecode(L, -1, nestingLimit); + return true; + } + else + { + fprintf(stderr, "Error loading bytecode %s\n", name); + return false; + } + } + catch (Luau::ParseErrors& e) + { + for (auto& error : e.getErrors()) + reportError(name, error); + return false; + } + catch (Luau::CompileError& e) + { + reportError(name, e); + return false; + } + + return true; +} + +static std::string escapeFilename(const std::string& filename) +{ + std::string escaped; + escaped.reserve(filename.size()); + + for (const char ch : filename) + { + switch (ch) + { + case '\\': + escaped.push_back('/'); + break; + case '"': + escaped.push_back('\\'); + escaped.push_back(ch); + break; + default: + escaped.push_back(ch); + } + } + + return escaped; +} + +static void serializeFunctionSummary(const FunctionBytecodeSummary& summary, FILE* fp) +{ + const unsigned nestingLimit = summary.getNestingLimit(); + const unsigned opLimit = summary.getOpLimit(); + + fprintf(fp, " {\n"); + fprintf(fp, " \"source\": \"%s\",\n", summary.getSource().c_str()); + fprintf(fp, " \"name\": \"%s\",\n", summary.getName().c_str()); + fprintf(fp, " \"line\": %d,\n", summary.getLine()); + fprintf(fp, " \"nestingLimit\": %u,\n", nestingLimit); + fprintf(fp, " \"counts\": ["); + + for (unsigned nesting = 0; nesting <= nestingLimit; ++nesting) + { + fprintf(fp, "\n ["); + + for (unsigned i = 0; i < opLimit; ++i) + { + fprintf(fp, "%d", summary.getCount(nesting, uint8_t(i))); + if (i < opLimit - 1) + fprintf(fp, ", "); + } + + fprintf(fp, "]"); + if (nesting < nestingLimit) + fprintf(fp, ","); + } + + fprintf(fp, "\n ]"); + fprintf(fp, "\n }"); +} + +static void serializeScriptSummary(const std::string& file, const std::vector& scriptSummary, FILE* fp) +{ + std::string escaped(escapeFilename(file)); + const size_t functionCount = scriptSummary.size(); + + fprintf(fp, " \"%s\": [\n", escaped.c_str()); + + for (size_t i = 0; i < functionCount; ++i) + { + serializeFunctionSummary(scriptSummary[i], fp); + fprintf(fp, i == (functionCount - 1) ? "\n" : ",\n"); + } + + fprintf(fp, " ]"); +} + +static bool serializeSummaries( + const std::vector& files, const std::vector>& scriptSummaries, const std::string& summaryFile) +{ + + FILE* fp = fopen(summaryFile.c_str(), "w"); + const size_t fileCount = files.size(); + + if (!fp) + { + fprintf(stderr, "Unable to open '%s'.\n", summaryFile.c_str()); + return false; + } + + fprintf(fp, "{\n"); + + for (size_t i = 0; i < fileCount; ++i) + { + serializeScriptSummary(files[i], scriptSummaries[i], fp); + fprintf(fp, i < (fileCount - 1) ? ",\n" : "\n"); + } + + fprintf(fp, "}"); + fclose(fp); + + return true; +} + +static int assertionHandler(const char* expr, const char* file, int line, const char* function) +{ + printf("%s(%d): ASSERTION FAILED: %s\n", file, line, expr); + return 1; +} + +int main(int argc, char** argv) +{ + Luau::assertHandler() = assertionHandler; + + setLuauFlagsDefault(); + + std::string summaryFile("bytecode-summary.json"); + unsigned nestingLimit = 0; + + if (!parseArgs(argc, argv, summaryFile)) + return 1; + + const std::vector files = getSourceFiles(argc, argv); + size_t fileCount = files.size(); + + std::vector> scriptSummaries; + scriptSummaries.reserve(fileCount); + + for (size_t i = 0; i < fileCount; ++i) + { + if (!analyzeFile(files[i].c_str(), nestingLimit, scriptSummaries[i])) + return 1; + } + + if (!serializeSummaries(files, scriptSummaries, summaryFile)) + return 1; + + fprintf(stdout, "Bytecode summary written to '%s'\n", summaryFile.c_str()); + + return 0; +} diff --git a/CMakeLists.txt b/CMakeLists.txt index 0dbfbee1..9f906fb2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -37,6 +37,7 @@ if(LUAU_BUILD_CLI) add_executable(Luau.Ast.CLI) add_executable(Luau.Reduce.CLI) add_executable(Luau.Compile.CLI) + add_executable(Luau.Bytecode.CLI) # This also adds target `name` on Linux/macOS and `name.exe` on Windows set_target_properties(Luau.Repl.CLI PROPERTIES OUTPUT_NAME luau) @@ -44,6 +45,7 @@ if(LUAU_BUILD_CLI) set_target_properties(Luau.Ast.CLI PROPERTIES OUTPUT_NAME luau-ast) set_target_properties(Luau.Reduce.CLI PROPERTIES OUTPUT_NAME luau-reduce) set_target_properties(Luau.Compile.CLI PROPERTIES OUTPUT_NAME luau-compile) + set_target_properties(Luau.Bytecode.CLI PROPERTIES OUTPUT_NAME luau-bytecode) endif() if(LUAU_BUILD_TESTS) @@ -187,6 +189,7 @@ if(LUAU_BUILD_CLI) target_compile_options(Luau.Analyze.CLI PRIVATE ${LUAU_OPTIONS}) target_compile_options(Luau.Ast.CLI PRIVATE ${LUAU_OPTIONS}) target_compile_options(Luau.Compile.CLI PRIVATE ${LUAU_OPTIONS}) + target_compile_options(Luau.Bytecode.CLI PRIVATE ${LUAU_OPTIONS}) target_include_directories(Luau.Repl.CLI PRIVATE extern extern/isocline/include) @@ -209,6 +212,8 @@ if(LUAU_BUILD_CLI) target_link_libraries(Luau.Reduce.CLI PRIVATE Luau.Common Luau.Ast Luau.Analysis) target_link_libraries(Luau.Compile.CLI PRIVATE Luau.Compiler Luau.VM Luau.CodeGen) + + target_link_libraries(Luau.Bytecode.CLI PRIVATE Luau.Compiler Luau.VM Luau.CodeGen) endif() if(LUAU_BUILD_TESTS) diff --git a/CodeGen/include/Luau/BytecodeSummary.h b/CodeGen/include/Luau/BytecodeSummary.h new file mode 100644 index 00000000..cfdd5f84 --- /dev/null +++ b/CodeGen/include/Luau/BytecodeSummary.h @@ -0,0 +1,81 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#pragma once + +#include "Luau/Common.h" +#include "Luau/Bytecode.h" + +#include +#include + +struct lua_State; +struct Proto; + +namespace Luau +{ +namespace CodeGen +{ + +class FunctionBytecodeSummary +{ +public: + FunctionBytecodeSummary(std::string source, std::string name, const int line, unsigned nestingLimit); + + const std::string& getSource() const + { + return source; + } + + const std::string& getName() const + { + return name; + } + + int getLine() const + { + return line; + } + + const unsigned getNestingLimit() const + { + return nestingLimit; + } + + const unsigned getOpLimit() const + { + return LOP__COUNT; + } + + void incCount(unsigned nesting, uint8_t op) + { + LUAU_ASSERT(nesting <= getNestingLimit()); + LUAU_ASSERT(op < getOpLimit()); + ++counts[nesting][op]; + } + + unsigned getCount(unsigned nesting, uint8_t op) const + { + LUAU_ASSERT(nesting <= getNestingLimit()); + LUAU_ASSERT(op < getOpLimit()); + return counts[nesting][op]; + } + + const std::vector& getCounts(unsigned nesting) const + { + LUAU_ASSERT(nesting <= getNestingLimit()); + return counts[nesting]; + } + + static FunctionBytecodeSummary fromProto(Proto* proto, unsigned nestingLimit); + +private: + std::string source; + std::string name; + int line; + unsigned nestingLimit; + std::vector> counts; +}; + +std::vector summarizeBytecode(lua_State* L, int idx, unsigned nestingLimit); + +} // namespace CodeGen +} // namespace Luau diff --git a/CodeGen/src/AssemblyBuilderX64.cpp b/CodeGen/src/AssemblyBuilderX64.cpp index 22978dd4..ec53916f 100644 --- a/CodeGen/src/AssemblyBuilderX64.cpp +++ b/CodeGen/src/AssemblyBuilderX64.cpp @@ -6,6 +6,8 @@ #include #include +LUAU_FASTFLAG(LuauCodeGenFixByteLower) + namespace Luau { namespace CodeGen @@ -1437,10 +1439,18 @@ void AssemblyBuilderX64::placeImm8(int32_t imm) { int8_t imm8 = int8_t(imm); - if (imm8 == imm) + if (FFlag::LuauCodeGenFixByteLower) + { + LUAU_ASSERT(imm8 == imm); place(imm8); + } else - LUAU_ASSERT(!"Invalid immediate value"); + { + if (imm8 == imm) + place(imm8); + else + LUAU_ASSERT(!"Invalid immediate value"); + } } void AssemblyBuilderX64::placeImm16(int16_t imm) diff --git a/CodeGen/src/BytecodeSummary.cpp b/CodeGen/src/BytecodeSummary.cpp new file mode 100644 index 00000000..7bf38cc4 --- /dev/null +++ b/CodeGen/src/BytecodeSummary.cpp @@ -0,0 +1,71 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#include "Luau/BytecodeSummary.h" +#include "CodeGenLower.h" + +#include "lua.h" +#include "lapi.h" +#include "lobject.h" +#include "lstate.h" + +namespace Luau +{ +namespace CodeGen +{ + +FunctionBytecodeSummary::FunctionBytecodeSummary(std::string source, std::string name, const int line, unsigned nestingLimit) + : source(std::move(source)) + , name(std::move(name)) + , line(line) + , nestingLimit(nestingLimit) +{ + counts.reserve(nestingLimit); + for (unsigned i = 0; i < 1 + nestingLimit; ++i) + { + counts.push_back(std::vector(getOpLimit(), 0)); + } +} + +FunctionBytecodeSummary FunctionBytecodeSummary::fromProto(Proto* proto, unsigned nestingLimit) +{ + const char* source = getstr(proto->source); + source = (source[0] == '=' || source[0] == '@') ? source + 1 : "[string]"; + + const char* name = proto->debugname ? getstr(proto->debugname) : ""; + + int line = proto->linedefined; + + FunctionBytecodeSummary summary(source, name, line, nestingLimit); + + for (int i = 0; i < proto->sizecode; ++i) + { + Instruction insn = proto->code[i]; + uint8_t op = LUAU_INSN_OP(insn); + summary.incCount(0, op); + } + + return summary; +} + +std::vector summarizeBytecode(lua_State* L, int idx, unsigned nestingLimit) +{ + LUAU_ASSERT(lua_isLfunction(L, idx)); + const TValue* func = luaA_toobject(L, idx); + + Proto* root = clvalue(func)->l.p; + + std::vector protos; + gatherFunctions(protos, root, CodeGen_ColdFunctions); + + std::vector summaries; + summaries.reserve(protos.size()); + + for (Proto* proto : protos) + { + summaries.push_back(FunctionBytecodeSummary::fromProto(proto, nestingLimit)); + } + + return summaries; +} + +} // namespace CodeGen +} // namespace Luau diff --git a/CodeGen/src/IrLoweringA64.cpp b/CodeGen/src/IrLoweringA64.cpp index 42450d3c..6a1733a5 100644 --- a/CodeGen/src/IrLoweringA64.cpp +++ b/CodeGen/src/IrLoweringA64.cpp @@ -405,7 +405,15 @@ void IrLoweringA64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next) case IrCmd::STORE_POINTER: { AddressA64 addr = tempAddr(inst.a, offsetof(TValue, value)); - build.str(regOp(inst.b), addr); + if (inst.b.kind == IrOpKind::Constant) + { + LUAU_ASSERT(intOp(inst.b) == 0); + build.str(xzr, addr); + } + else + { + build.str(regOp(inst.b), addr); + } break; } case IrCmd::STORE_DOUBLE: diff --git a/CodeGen/src/IrLoweringX64.cpp b/CodeGen/src/IrLoweringX64.cpp index f7572a6c..74a5bfd6 100644 --- a/CodeGen/src/IrLoweringX64.cpp +++ b/CodeGen/src/IrLoweringX64.cpp @@ -15,6 +15,8 @@ #include "lstate.h" #include "lgc.h" +LUAU_FASTFLAG(LuauCodeGenFixByteLower) + namespace Luau { namespace CodeGen @@ -213,11 +215,24 @@ void IrLoweringX64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next) LUAU_ASSERT(!"Unsupported instruction form"); break; case IrCmd::STORE_POINTER: - if (inst.a.kind == IrOpKind::Inst) - build.mov(qword[regOp(inst.a) + offsetof(TValue, value)], regOp(inst.b)); + { + OperandX64 valueLhs = inst.a.kind == IrOpKind::Inst ? qword[regOp(inst.a) + offsetof(TValue, value)] : luauRegValue(vmRegOp(inst.a)); + + if (inst.b.kind == IrOpKind::Constant) + { + LUAU_ASSERT(intOp(inst.b) == 0); + build.mov(valueLhs, 0); + } + else if (inst.b.kind == IrOpKind::Inst) + { + build.mov(valueLhs, regOp(inst.b)); + } else - build.mov(luauRegValue(vmRegOp(inst.a)), regOp(inst.b)); + { + LUAU_ASSERT(!"Unsupported instruction form"); + } break; + } case IrCmd::STORE_DOUBLE: { OperandX64 valueLhs = inst.a.kind == IrOpKind::Inst ? qword[regOp(inst.a) + offsetof(TValue, value)] : luauRegValue(vmRegOp(inst.a)); @@ -1787,9 +1802,18 @@ void IrLoweringX64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next) case IrCmd::BUFFER_WRITEI8: { - OperandX64 value = inst.c.kind == IrOpKind::Inst ? byteReg(regOp(inst.c)) : OperandX64(intOp(inst.c)); + if (FFlag::LuauCodeGenFixByteLower) + { + OperandX64 value = inst.c.kind == IrOpKind::Inst ? byteReg(regOp(inst.c)) : OperandX64(int8_t(intOp(inst.c))); - build.mov(byte[bufferAddrOp(inst.a, inst.b)], value); + build.mov(byte[bufferAddrOp(inst.a, inst.b)], value); + } + else + { + OperandX64 value = inst.c.kind == IrOpKind::Inst ? byteReg(regOp(inst.c)) : OperandX64(intOp(inst.c)); + + build.mov(byte[bufferAddrOp(inst.a, inst.b)], value); + } break; } @@ -1807,9 +1831,18 @@ void IrLoweringX64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next) case IrCmd::BUFFER_WRITEI16: { - OperandX64 value = inst.c.kind == IrOpKind::Inst ? wordReg(regOp(inst.c)) : OperandX64(intOp(inst.c)); + if (FFlag::LuauCodeGenFixByteLower) + { + OperandX64 value = inst.c.kind == IrOpKind::Inst ? wordReg(regOp(inst.c)) : OperandX64(int16_t(intOp(inst.c))); - build.mov(word[bufferAddrOp(inst.a, inst.b)], value); + build.mov(word[bufferAddrOp(inst.a, inst.b)], value); + } + else + { + OperandX64 value = inst.c.kind == IrOpKind::Inst ? wordReg(regOp(inst.c)) : OperandX64(intOp(inst.c)); + + build.mov(word[bufferAddrOp(inst.a, inst.b)], value); + } break; } diff --git a/CodeGen/src/IrTranslation.cpp b/CodeGen/src/IrTranslation.cpp index dff7002d..91e87fdb 100644 --- a/CodeGen/src/IrTranslation.cpp +++ b/CodeGen/src/IrTranslation.cpp @@ -14,6 +14,7 @@ LUAU_FASTFLAGVARIABLE(LuauLowerAltLoopForn, false) LUAU_FASTFLAG(LuauImproveInsertIr) +LUAU_FASTFLAGVARIABLE(LuauFullLoopLuserdata, false) namespace Luau { @@ -808,7 +809,7 @@ void translateInstForGPrepNext(IrBuilder& build, const Instruction* pc, int pcpo build.inst(IrCmd::STORE_TAG, build.vmReg(ra), build.constTag(LUA_TNIL)); // setpvalue(ra + 2, reinterpret_cast(uintptr_t(0))); - build.inst(IrCmd::STORE_INT, build.vmReg(ra + 2), build.constInt(0)); + build.inst(FFlag::LuauFullLoopLuserdata ? IrCmd::STORE_POINTER : IrCmd::STORE_INT, build.vmReg(ra + 2), build.constInt(0)); build.inst(IrCmd::STORE_TAG, build.vmReg(ra + 2), build.constTag(LUA_TLIGHTUSERDATA)); build.inst(IrCmd::JUMP, target); @@ -840,7 +841,7 @@ void translateInstForGPrepInext(IrBuilder& build, const Instruction* pc, int pcp build.inst(IrCmd::STORE_TAG, build.vmReg(ra), build.constTag(LUA_TNIL)); // setpvalue(ra + 2, reinterpret_cast(uintptr_t(0))); - build.inst(IrCmd::STORE_INT, build.vmReg(ra + 2), build.constInt(0)); + build.inst(FFlag::LuauFullLoopLuserdata ? IrCmd::STORE_POINTER : IrCmd::STORE_INT, build.vmReg(ra + 2), build.constInt(0)); build.inst(IrCmd::STORE_TAG, build.vmReg(ra + 2), build.constTag(LUA_TLIGHTUSERDATA)); build.inst(IrCmd::JUMP, target); diff --git a/CodeGen/src/OptimizeConstProp.cpp b/CodeGen/src/OptimizeConstProp.cpp index 8d0f829a..45583fcc 100644 --- a/CodeGen/src/OptimizeConstProp.cpp +++ b/CodeGen/src/OptimizeConstProp.cpp @@ -17,6 +17,7 @@ LUAU_FASTINTVARIABLE(LuauCodeGenReuseSlotLimit, 64) LUAU_FASTFLAGVARIABLE(DebugLuauAbortingChecks, false) LUAU_FASTFLAGVARIABLE(LuauReuseArrSlots2, false) LUAU_FASTFLAG(LuauLowerAltLoopForn) +LUAU_FASTFLAGVARIABLE(LuauCodeGenFixByteLower, false) namespace Luau { @@ -618,15 +619,19 @@ static void constPropInInst(ConstPropState& state, IrBuilder& build, IrFunction& if (inst.a.kind == IrOpKind::VmReg) { state.invalidateValue(inst.a); - state.forwardVmRegStoreToLoad(inst, IrCmd::LOAD_POINTER); - if (IrInst* instOp = function.asInstOp(inst.b); instOp && instOp->cmd == IrCmd::NEW_TABLE) + if (inst.b.kind == IrOpKind::Inst) { - if (RegisterInfo* info = state.tryGetRegisterInfo(inst.a)) + state.forwardVmRegStoreToLoad(inst, IrCmd::LOAD_POINTER); + + if (IrInst* instOp = function.asInstOp(inst.b); instOp && instOp->cmd == IrCmd::NEW_TABLE) { - info->knownNotReadonly = true; - info->knownNoMetatable = true; - info->knownTableArraySize = function.uintOp(instOp->a); + if (RegisterInfo* info = state.tryGetRegisterInfo(inst.a)) + { + info->knownNotReadonly = true; + info->knownNoMetatable = true; + info->knownTableArraySize = function.uintOp(instOp->a); + } } } } diff --git a/Common/include/Luau/DenseHash.h b/Common/include/Luau/DenseHash.h index 067a9d7a..f175b169 100644 --- a/Common/include/Luau/DenseHash.h +++ b/Common/include/Luau/DenseHash.h @@ -540,7 +540,7 @@ public: return impl.end(); } - bool operator==(const DenseHashSet& other) + bool operator==(const DenseHashSet& other) const { if (size() != other.size()) return false; @@ -554,7 +554,7 @@ public: return true; } - bool operator!=(const DenseHashSet& other) + bool operator!=(const DenseHashSet& other) const { return !(*this == other); } diff --git a/Makefile b/Makefile index 9e97633f..4d606bef 100644 --- a/Makefile +++ b/Makefile @@ -54,6 +54,10 @@ COMPILE_CLI_SOURCES=CLI/FileUtils.cpp CLI/Flags.cpp CLI/Compile.cpp COMPILE_CLI_OBJECTS=$(COMPILE_CLI_SOURCES:%=$(BUILD)/%.o) COMPILE_CLI_TARGET=$(BUILD)/luau-compile +BYTECODE_CLI_SOURCES=CLI/FileUtils.cpp CLI/Flags.cpp CLI/Bytecode.cpp +BYTECODE_CLI_OBJECTS=$(BYTECODE_CLI_SOURCES:%=$(BUILD)/%.o) +BYTECODE_CLI_TARGET=$(BUILD)/luau-bytecode + FUZZ_SOURCES=$(wildcard fuzz/*.cpp) fuzz/luau.pb.cpp FUZZ_OBJECTS=$(FUZZ_SOURCES:%=$(BUILD)/%.o) @@ -65,8 +69,8 @@ ifneq ($(opt),) TESTS_ARGS+=-O$(opt) endif -OBJECTS=$(AST_OBJECTS) $(COMPILER_OBJECTS) $(CONFIG_OBJECTS) $(ANALYSIS_OBJECTS) $(CODEGEN_OBJECTS) $(VM_OBJECTS) $(ISOCLINE_OBJECTS) $(TESTS_OBJECTS) $(REPL_CLI_OBJECTS) $(ANALYZE_CLI_OBJECTS) $(COMPILE_CLI_OBJECTS) $(FUZZ_OBJECTS) -EXECUTABLE_ALIASES = luau luau-analyze luau-compile luau-tests +OBJECTS=$(AST_OBJECTS) $(COMPILER_OBJECTS) $(CONFIG_OBJECTS) $(ANALYSIS_OBJECTS) $(CODEGEN_OBJECTS) $(VM_OBJECTS) $(ISOCLINE_OBJECTS) $(TESTS_OBJECTS) $(REPL_CLI_OBJECTS) $(ANALYZE_CLI_OBJECTS) $(COMPILE_CLI_OBJECTS) $(BYTECODE_CLI_OBJECTS) $(FUZZ_OBJECTS) +EXECUTABLE_ALIASES = luau luau-analyze luau-compile luau-bytecode luau-tests # common flags CXXFLAGS=-g -Wall @@ -142,6 +146,7 @@ $(TESTS_OBJECTS): CXXFLAGS+=-std=c++17 -ICommon/include -IAst/include -ICompiler $(REPL_CLI_OBJECTS): CXXFLAGS+=-std=c++17 -ICommon/include -IAst/include -ICompiler/include -IVM/include -ICodeGen/include -Iextern -Iextern/isocline/include $(ANALYZE_CLI_OBJECTS): CXXFLAGS+=-std=c++17 -ICommon/include -IAst/include -IAnalysis/include -IConfig/include -Iextern $(COMPILE_CLI_OBJECTS): CXXFLAGS+=-std=c++17 -ICommon/include -IAst/include -ICompiler/include -IVM/include -ICodeGen/include +$(BYTECODE_CLI_OBJECTS): CXXFLAGS+=-std=c++17 -ICommon/include -IAst/include -ICompiler/include -IVM/include -ICodeGen/include $(FUZZ_OBJECTS): CXXFLAGS+=-std=c++17 -ICommon/include -IAst/include -ICompiler/include -IAnalysis/include -IVM/include -ICodeGen/include -IConfig/include $(TESTS_TARGET): LDFLAGS+=-lpthread @@ -206,6 +211,9 @@ luau-analyze: $(ANALYZE_CLI_TARGET) luau-compile: $(COMPILE_CLI_TARGET) ln -fs $^ $@ +luau-bytecode: $(BYTECODE_CLI_TARGET) + ln -fs $^ $@ + luau-tests: $(TESTS_TARGET) ln -fs $^ $@ @@ -214,8 +222,9 @@ $(TESTS_TARGET): $(TESTS_OBJECTS) $(ANALYSIS_TARGET) $(COMPILER_TARGET) $(CONFIG $(REPL_CLI_TARGET): $(REPL_CLI_OBJECTS) $(COMPILER_TARGET) $(AST_TARGET) $(CODEGEN_TARGET) $(VM_TARGET) $(ISOCLINE_TARGET) $(ANALYZE_CLI_TARGET): $(ANALYZE_CLI_OBJECTS) $(ANALYSIS_TARGET) $(AST_TARGET) $(CONFIG_TARGET) $(COMPILE_CLI_TARGET): $(COMPILE_CLI_OBJECTS) $(COMPILER_TARGET) $(AST_TARGET) $(CODEGEN_TARGET) $(VM_TARGET) +$(BYTECODE_CLI_TARGET): $(BYTECODE_CLI_OBJECTS) $(COMPILER_TARGET) $(AST_TARGET) $(CODEGEN_TARGET) $(VM_TARGET) -$(TESTS_TARGET) $(REPL_CLI_TARGET) $(ANALYZE_CLI_TARGET) $(COMPILE_CLI_TARGET): +$(TESTS_TARGET) $(REPL_CLI_TARGET) $(ANALYZE_CLI_TARGET) $(COMPILE_CLI_TARGET) $(BYTECODE_CLI_TARGET): $(CXX) $^ $(LDFLAGS) -o $@ # executable targets for fuzzing diff --git a/Sources.cmake b/Sources.cmake index 929c99a4..eee66b86 100644 --- a/Sources.cmake +++ b/Sources.cmake @@ -92,6 +92,7 @@ target_sources(Luau.CodeGen PRIVATE CodeGen/include/Luau/UnwindBuilder.h CodeGen/include/Luau/UnwindBuilderDwarf2.h CodeGen/include/Luau/UnwindBuilderWin.h + CodeGen/include/Luau/BytecodeSummary.h CodeGen/include/luacodegen.h CodeGen/src/AssemblyBuilderA64.cpp @@ -124,6 +125,7 @@ target_sources(Luau.CodeGen PRIVATE CodeGen/src/OptimizeFinalX64.cpp CodeGen/src/UnwindBuilderDwarf2.cpp CodeGen/src/UnwindBuilderWin.cpp + CodeGen/src/BytecodeSummary.cpp CodeGen/src/BitUtils.h CodeGen/src/ByteUtils.h @@ -518,3 +520,13 @@ if(TARGET Luau.Compile.CLI) CLI/Flags.cpp CLI/Compile.cpp) endif() + +if(TARGET Luau.Bytecode.CLI) + # Luau.Bytecode.CLI Sources + target_sources(Luau.Bytecode.CLI PRIVATE + CLI/FileUtils.h + CLI/FileUtils.cpp + CLI/Flags.h + CLI/Flags.cpp + CLI/Bytecode.cpp) +endif() diff --git a/tests/Conformance.test.cpp b/tests/Conformance.test.cpp index 968a55be..db4941ee 100644 --- a/tests/Conformance.test.cpp +++ b/tests/Conformance.test.cpp @@ -10,7 +10,9 @@ #include "Luau/TypeInfer.h" #include "Luau/BytecodeBuilder.h" #include "Luau/Frontend.h" +#include "Luau/Compiler.h" #include "Luau/CodeGen.h" +#include "Luau/BytecodeSummary.h" #include "doctest.h" #include "ScopedFlags.h" @@ -271,6 +273,25 @@ static void* limitedRealloc(void* ud, void* ptr, size_t osize, size_t nsize) } } +static std::vector analyzeFile(const char* source, const unsigned nestingLimit) +{ + Luau::BytecodeBuilder bcb; + + Luau::CompileOptions options; + options.optimizationLevel = optimizationLevel; + options.debugLevel = 1; + + compileOrThrow(bcb, source, options); + + const std::string& bytecode = bcb.getBytecode(); + + std::unique_ptr globalState(luaL_newstate(), lua_close); + lua_State* L = globalState.get(); + + LUAU_ASSERT(luau_load(L, "source", bytecode.data(), bytecode.size(), 0) == 0); + return Luau::CodeGen::summarizeBytecode(L, -1, nestingLimit); +} + TEST_SUITE_BEGIN("Conformance"); TEST_CASE("CodegenSupported") @@ -292,6 +313,7 @@ TEST_CASE("Basic") TEST_CASE("Buffers") { ScopedFastFlag luauBufferBetterMsg{"LuauBufferBetterMsg", true}; + ScopedFastFlag luauCodeGenFixByteLower{"LuauCodeGenFixByteLower", true}; runConformance("buffers.lua"); } @@ -1988,4 +2010,51 @@ TEST_CASE("HugeFunction") CHECK(lua_tonumber(L, -1) == 42); } +TEST_CASE("BytecodeDistributionPerFunctionTest") +{ + const char* source = R"( +local function first(n, p) + local t = {} + for i=1,p do t[i] = i*10 end + + local function inner(_,n) + if n > 0 then + n = n-1 + return n, unpack(t) + end + end + return inner, nil, n +end + +local function second(x) + return x[1] +end +)"; + + std::vector summaries(analyzeFile(source, 0)); + + CHECK_EQ(summaries[0].getName(), "inner"); + CHECK_EQ(summaries[0].getLine(), 6); + CHECK_EQ(summaries[0].getCounts(0), + std::vector({1, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0})); + + CHECK_EQ(summaries[1].getName(), "first"); + CHECK_EQ(summaries[1].getLine(), 2); + CHECK_EQ(summaries[1].getCounts(0), + std::vector({1, 0, 1, 0, 2, 0, 3, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0})); + + CHECK_EQ(summaries[2].getName(), "second"); + CHECK_EQ(summaries[2].getLine(), 15); + CHECK_EQ(summaries[2].getCounts(0), + std::vector({0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0})); + + CHECK_EQ(summaries[3].getName(), ""); + CHECK_EQ(summaries[3].getLine(), 1); + CHECK_EQ(summaries[3].getCounts(0), + std::vector({0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0})); +} + TEST_SUITE_END(); diff --git a/tests/DataFlowGraph.test.cpp b/tests/DataFlowGraph.test.cpp index e957316e..be0337a2 100644 --- a/tests/DataFlowGraph.test.cpp +++ b/tests/DataFlowGraph.test.cpp @@ -317,4 +317,97 @@ TEST_CASE_FIXTURE(DataFlowGraphFixture, "mutate_property_of_table_owned_by_while CHECK(x1 != x2); } +TEST_CASE_FIXTURE(DataFlowGraphFixture, "property_lookup_on_a_phi_node") +{ + dfg(R"( + local t = {} + t.x = 5 + + if cond() then + t.x = 7 + end + + print(t.x) + )"); + + DefId x1 = getDef(); // t.x = 5 + DefId x2 = getDef(); // t.x = 7 + DefId x3 = getDef(); // print(t.x) + + CHECK(x1 != x2); + CHECK(x2 != x3); + + const Phi* phi = get(x3); + REQUIRE(phi); + CHECK(phi->operands.at(0) == x1); + CHECK(phi->operands.at(1) == x2); +} + +TEST_CASE_FIXTURE(DataFlowGraphFixture, "property_lookup_on_a_phi_node_2") +{ + dfg(R"( + local t = {} + + if cond() then + t.x = 5 + else + t.x = 7 + end + + print(t.x) + )"); + + DefId x1 = getDef(); // t.x = 5 + DefId x2 = getDef(); // t.x = 7 + DefId x3 = getDef(); // print(t.x) + + CHECK(x1 != x2); + CHECK(x2 != x3); + + const Phi* phi = get(x3); + REQUIRE(phi); + CHECK(phi->operands.at(0) == x2); + CHECK(phi->operands.at(1) == x1); +} + +TEST_CASE_FIXTURE(DataFlowGraphFixture, "property_lookup_on_a_phi_node_3") +{ + dfg(R"( + local t = {} + t.x = 3 + + if cond() then + t.x = 5 + t.y = 7 + else + t.z = 42 + end + + print(t.x) + print(t.y) + print(t.z) + )"); + + DefId x1 = getDef(); // t.x = 3 + DefId x2 = getDef(); // t.x = 5 + + DefId y1 = getDef(); // t.y = 7 + + DefId z1 = getDef(); // t.z = 42 + + DefId x3 = getDef(); // print(t.x) + DefId y2 = getDef(); // print(t.y) + DefId z2 = getDef(); // print(t.z) + + CHECK(x1 != x2); + CHECK(x2 != x3); + CHECK(y1 == y2); + CHECK(z1 == z2); + + const Phi* phi = get(x3); + REQUIRE(phi); + CHECK(phi->operands.at(0) == x1); + CHECK(phi->operands.at(1) == x2); +} + TEST_SUITE_END(); diff --git a/tests/Fixture.cpp b/tests/Fixture.cpp index 8f40f574..b3a46e49 100644 --- a/tests/Fixture.cpp +++ b/tests/Fixture.cpp @@ -412,7 +412,7 @@ TypeId Fixture::requireTypeAlias(const std::string& name) { std::optional ty = lookupType(name); REQUIRE(ty); - return *ty; + return follow(*ty); } TypeId Fixture::requireExportedType(const ModuleName& moduleName, const std::string& name) diff --git a/tests/NonStrictTypeChecker.test.cpp b/tests/NonStrictTypeChecker.test.cpp index 8eb778c8..e65635bd 100644 --- a/tests/NonStrictTypeChecker.test.cpp +++ b/tests/NonStrictTypeChecker.test.cpp @@ -6,12 +6,22 @@ #include "Luau/Common.h" #include "Luau/Ast.h" #include "Luau/ModuleResolver.h" +#include "Luau/VisitType.h" #include "ScopedFlags.h" #include "doctest.h" #include using namespace Luau; +#define NONSTRICT_REQUIRE_CHECKED_ERR(index, name, result) \ + do \ + { \ + REQUIRE(index < result.errors.size()); \ + auto err##index = get(result.errors[index]); \ + REQUIRE(err##index != nullptr); \ + CHECK_EQ((err##index)->checkedFunctionName, name); \ + } while (false) + struct NonStrictTypeCheckerFixture : Fixture { @@ -28,22 +38,167 @@ struct NonStrictTypeCheckerFixture : Fixture std::string definitions = R"BUILTIN_SRC( declare function @checked abs(n: number): number +declare function @checked lower(s: string): string +declare function cond() : boolean )BUILTIN_SRC"; }; TEST_SUITE_BEGIN("NonStrictTypeCheckerTest"); -TEST_CASE_FIXTURE(NonStrictTypeCheckerFixture, "simple_non_strict") +TEST_CASE_FIXTURE(NonStrictTypeCheckerFixture, "simple_non_strict_failure") { - auto res = checkNonStrict(R"BUILTIN_SRC( + CheckResult result = checkNonStrict(R"BUILTIN_SRC( abs("hi") )BUILTIN_SRC"); - LUAU_REQUIRE_ERRORS(res); - REQUIRE(res.errors.size() == 1); - auto err = get(res.errors[0]); - REQUIRE(err != nullptr); - REQUIRE(err->checkedFunctionName == "abs"); + LUAU_REQUIRE_ERROR_COUNT(1, result); + NONSTRICT_REQUIRE_CHECKED_ERR(0, "abs", result); } +TEST_CASE_FIXTURE(NonStrictTypeCheckerFixture, "nested_function_calls_constant") +{ + CheckResult result = checkNonStrict(R"( +local x +abs(lower(x)) +)"); + + LUAU_REQUIRE_ERROR_COUNT(1, result); + NONSTRICT_REQUIRE_CHECKED_ERR(0, "abs", result); +} + + +TEST_CASE_FIXTURE(NonStrictTypeCheckerFixture, "if_then_else_warns_with_never_local") +{ + CheckResult result = checkNonStrict(R"( +local x : never +if cond() then + abs(x) +else + lower(x) +end +)"); + + LUAU_REQUIRE_ERROR_COUNT(2, result); + NONSTRICT_REQUIRE_CHECKED_ERR(0, "abs", result); + NONSTRICT_REQUIRE_CHECKED_ERR(1, "lower", result); +} + +TEST_CASE_FIXTURE(NonStrictTypeCheckerFixture, "if_then_else_warns_nil_branches") +{ + auto result = checkNonStrict(R"( +local x +if cond() then + abs(x) +else + lower(x) +end +)"); + + LUAU_REQUIRE_ERROR_COUNT(2, result); + NONSTRICT_REQUIRE_CHECKED_ERR(0, "abs", result); + NONSTRICT_REQUIRE_CHECKED_ERR(1, "lower", result); +} + +TEST_CASE_FIXTURE(NonStrictTypeCheckerFixture, "if_then_else_doesnt_warn_else_branch") +{ + auto result = checkNonStrict(R"( +local x : string = "hi" +if cond() then + abs(x) +else + lower(x) +end +)"); + + LUAU_REQUIRE_ERROR_COUNT(1, result); + NONSTRICT_REQUIRE_CHECKED_ERR(0, "abs", result); +} + +TEST_CASE_FIXTURE(NonStrictTypeCheckerFixture, "if_then_no_else") +{ + CheckResult result = checkNonStrict(R"( +local x : string +if cond() then + abs(x) +end +)"); + + LUAU_REQUIRE_NO_ERRORS(result); +} + +TEST_CASE_FIXTURE(NonStrictTypeCheckerFixture, "if_then_no_else_err_in_cond") +{ + CheckResult result = checkNonStrict(R"( +local x : string +if abs(x) then + lower(x) +end +)"); + LUAU_REQUIRE_ERROR_COUNT(1, result); + NONSTRICT_REQUIRE_CHECKED_ERR(0, "abs", result); +} + +TEST_CASE_FIXTURE(NonStrictTypeCheckerFixture, "if_then_else_expr_should_warn") +{ + CheckResult result = checkNonStrict(R"( +local x : never +local y = if cond() then abs(x) else lower(x) +)"); + LUAU_REQUIRE_ERROR_COUNT(2, result); + NONSTRICT_REQUIRE_CHECKED_ERR(0, "abs", result); + NONSTRICT_REQUIRE_CHECKED_ERR(1, "lower", result); +} + +TEST_CASE_FIXTURE(NonStrictTypeCheckerFixture, "if_then_else_expr_doesnt_warn_else_branch") +{ + CheckResult result = checkNonStrict(R"( +local x : string = "hi" +local y = if cond() then abs(x) else lower(x) +)"); + LUAU_REQUIRE_ERROR_COUNT(1, result); + NONSTRICT_REQUIRE_CHECKED_ERR(0, "abs", result); +} + + +TEST_CASE_FIXTURE(NonStrictTypeCheckerFixture, "sequencing_errors") +{ + CheckResult result = checkNonStrict(R"( +function f(x) + abs(x) + lower(x) +end +)"); + LUAU_REQUIRE_ERROR_COUNT(2, result); + NONSTRICT_REQUIRE_CHECKED_ERR(0, "abs", result); + NONSTRICT_REQUIRE_CHECKED_ERR(1, "lower", result); +} + +TEST_CASE_FIXTURE(NonStrictTypeCheckerFixture, "sequencing_if_checked_call") +{ + CheckResult result = checkNonStrict(R"( +local x +if cond() then + x = 5 +else + x = nil +end +lower(x) +)"); + LUAU_REQUIRE_ERROR_COUNT(1, result); + NONSTRICT_REQUIRE_CHECKED_ERR(0, "lower", result); +} + +TEST_CASE_FIXTURE(NonStrictTypeCheckerFixture, "sequencing_unrelated_checked_calls") +{ + CheckResult result = checkNonStrict(R"( +function h(x, y) + abs(x) + lower(y) +end +)"); + + LUAU_REQUIRE_NO_ERRORS(result); +} + + TEST_SUITE_END(); diff --git a/tests/Set.test.cpp b/tests/Set.test.cpp index 4476452a..a70f4f61 100644 --- a/tests/Set.test.cpp +++ b/tests/Set.test.cpp @@ -60,4 +60,43 @@ TEST_CASE("erase_works_and_decreases_size") CHECK(!s1.contains(2)); } +TEST_CASE("iterate_over_set") +{ + Luau::Set s1{0}; + s1.insert(1); + s1.insert(2); + s1.insert(3); + REQUIRE(s1.size() == 3); + + int sum = 0; + + for (int e : s1) + sum += e; + + CHECK(sum == 6); +} + +TEST_CASE("iterate_over_set_skips_erased_elements") +{ + Luau::Set s1{0}; + s1.insert(1); + s1.insert(2); + s1.insert(3); + s1.insert(4); + s1.insert(5); + s1.insert(6); + REQUIRE(s1.size() == 6); + + s1.erase(2); + s1.erase(4); + s1.erase(6); + + int sum = 0; + + for (int e : s1) + sum += e; + + CHECK(sum == 9); +} + TEST_SUITE_END(); diff --git a/tests/Subtyping.test.cpp b/tests/Subtyping.test.cpp index d7120bca..cda1fbf5 100644 --- a/tests/Subtyping.test.cpp +++ b/tests/Subtyping.test.cpp @@ -10,6 +10,7 @@ #include "doctest.h" #include "Fixture.h" #include "RegisterCallbacks.h" + #include using namespace Luau; @@ -17,9 +18,38 @@ using namespace Luau; namespace Luau { +std::ostream& operator<<(std::ostream& lhs, const SubtypingVariance& variance) +{ + switch (variance) + { + case SubtypingVariance::Covariant: + return lhs << "covariant"; + case SubtypingVariance::Invariant: + return lhs << "invariant"; + case SubtypingVariance::Invalid: + return lhs << "*invalid*"; + } + + return lhs; +} + std::ostream& operator<<(std::ostream& lhs, const SubtypingReasoning& reasoning) { - return lhs << toString(reasoning.subPath) << " & set, const std::vector& items) +{ + if (items.size() != set.size()) + return false; + + for (const SubtypingReasoning& r : items) + { + if (!set.contains(r)) + return false; + } + + return true; } }; // namespace Luau @@ -1105,20 +1135,6 @@ TEST_SUITE_END(); TEST_SUITE_BEGIN("Subtyping.Subpaths"); -bool operator==(const DenseHashSet& set, const std::vector& items) -{ - if (items.size() != set.size()) - return false; - - for (const SubtypingReasoning& r : items) - { - if (!set.contains(r)) - return false; - } - - return true; -} - TEST_CASE_FIXTURE(SubtypeFixture, "table_property") { TypeId subTy = tbl({{"X", builtinTypes->numberType}}); @@ -1126,10 +1142,9 @@ TEST_CASE_FIXTURE(SubtypeFixture, "table_property") SubtypingResult result = isSubtype(subTy, superTy); CHECK(!result.isSubtype); - CHECK(result.reasoning == std::vector{SubtypingReasoning{ - /* subPath */ Path(TypePath::Property("X")), + CHECK(result.reasoning == std::vector{SubtypingReasoning{/* subPath */ Path(TypePath::Property("X")), /* superPath */ Path(TypePath::Property("X")), - }}); + /* variance */ SubtypingVariance::Invariant}}); } TEST_CASE_FIXTURE(SubtypeFixture, "table_indexers") @@ -1142,10 +1157,12 @@ TEST_CASE_FIXTURE(SubtypeFixture, "table_indexers") CHECK(result.reasoning == std::vector{SubtypingReasoning{ /* subPath */ Path(TypePath::TypeField::IndexLookup), /* superPath */ Path(TypePath::TypeField::IndexLookup), + /* variance */ SubtypingVariance::Invariant, }, SubtypingReasoning{ /* subPath */ Path(TypePath::TypeField::IndexResult), /* superPath */ Path(TypePath::TypeField::IndexResult), + /* variance */ SubtypingVariance::Invariant, }}); } @@ -1211,6 +1228,7 @@ TEST_CASE_FIXTURE(SubtypeFixture, "nested_table_properties") CHECK(result.reasoning == std::vector{SubtypingReasoning{ /* subPath */ TypePath::PathBuilder().prop("X").prop("Y").prop("Z").build(), /* superPath */ TypePath::PathBuilder().prop("X").prop("Y").prop("Z").build(), + /* variance */ SubtypingVariance::Invariant, }}); } @@ -1252,8 +1270,10 @@ TEST_CASE_FIXTURE(SubtypeFixture, "multiple_reasonings") SubtypingResult result = isSubtype(subTy, superTy); CHECK(!result.isSubtype); CHECK(result.reasoning == std::vector{ - SubtypingReasoning{/* subPath */ Path(TypePath::Property("X")), /* superPath */ Path(TypePath::Property("X"))}, - SubtypingReasoning{/* subPath */ Path(TypePath::Property("Y")), /* superPath */ Path(TypePath::Property("Y"))}, + SubtypingReasoning{/* subPath */ Path(TypePath::Property("X")), /* superPath */ Path(TypePath::Property("X")), + /* variance */ SubtypingVariance::Invariant}, + SubtypingReasoning{/* subPath */ Path(TypePath::Property("Y")), /* superPath */ Path(TypePath::Property("Y")), + /* variance */ SubtypingVariance::Invariant}, }); } diff --git a/tests/ToString.test.cpp b/tests/ToString.test.cpp index 00c3c737..be9fc75d 100644 --- a/tests/ToString.test.cpp +++ b/tests/ToString.test.cpp @@ -938,7 +938,7 @@ TEST_CASE_FIXTURE(Fixture, "tostring_error_mismatch") //clang-format off std::string expected = (FFlag::DebugLuauDeferredConstraintResolution) - ? R"(Type pack '{| a: number, b: string, c: {| d: string |} |}' could not be converted into '{ a: number, b: string, c: { d: number } }'; at [0]["c"]["d"], string is not a subtype of number)" + ? R"(Type pack '{| a: number, b: string, c: {| d: string |} |}' could not be converted into '{ a: number, b: string, c: { d: number } }'; at [0]["c"]["d"], string is not exactly number)" : R"(Type '{ a: number, b: string, c: { d: string } }' diff --git a/tests/TypeInfer.aliases.test.cpp b/tests/TypeInfer.aliases.test.cpp index 199b1b22..598e2dd9 100644 --- a/tests/TypeInfer.aliases.test.cpp +++ b/tests/TypeInfer.aliases.test.cpp @@ -198,8 +198,7 @@ TEST_CASE_FIXTURE(Fixture, "generic_aliases") )"); LUAU_REQUIRE_ERROR_COUNT(1, result); - const std::string expected = - R"(Type 'bad' could not be converted into 'T'; at ["v"], string is not a subtype of number)"; + const std::string expected = R"(Type 'bad' could not be converted into 'T'; at ["v"], string is not exactly number)"; CHECK(result.errors[0].location == Location{{4, 31}, {4, 44}}); CHECK_EQ(expected, toString(result.errors[0])); } @@ -218,8 +217,7 @@ TEST_CASE_FIXTURE(Fixture, "dependent_generic_aliases") )"); LUAU_REQUIRE_ERROR_COUNT(1, result); - const std::string expected = - R"(Type 'bad' could not be converted into 'U'; at ["t"]["v"], string is not a subtype of number)"; + const std::string expected = R"(Type 'bad' could not be converted into 'U'; at ["t"]["v"], string is not exactly number)"; CHECK(result.errors[0].location == Location{{4, 31}, {4, 52}}); CHECK_EQ(expected, toString(result.errors[0])); diff --git a/tests/TypeInfer.anyerror.test.cpp b/tests/TypeInfer.anyerror.test.cpp index 1e3db820..5d6b9b16 100644 --- a/tests/TypeInfer.anyerror.test.cpp +++ b/tests/TypeInfer.anyerror.test.cpp @@ -50,7 +50,15 @@ TEST_CASE_FIXTURE(Fixture, "for_in_loop_iterator_returns_any2") LUAU_REQUIRE_NO_ERRORS(result); - CHECK_EQ("any", toString(requireType("a"))); + if (FFlag::DebugLuauDeferredConstraintResolution) + { + // Bug: We do not simplify at the right time + CHECK_EQ("any?", toString(requireType("a"))); + } + else + { + CHECK_EQ("any", toString(requireType("a"))); + } } TEST_CASE_FIXTURE(Fixture, "for_in_loop_iterator_is_any") diff --git a/tests/TypeInfer.classes.test.cpp b/tests/TypeInfer.classes.test.cpp index a6d3fa5c..b53f60f0 100644 --- a/tests/TypeInfer.classes.test.cpp +++ b/tests/TypeInfer.classes.test.cpp @@ -111,7 +111,7 @@ TEST_CASE_FIXTURE(ClassFixture, "we_can_report_when_someone_is_trying_to_use_a_t )"); LUAU_REQUIRE_ERROR_COUNT(1, result); - TypeMismatch* tm = get(result.errors[0]); + TypeMismatch* tm = get(result.errors.at(0)); REQUIRE(tm != nullptr); CHECK_EQ("Oopsies", toString(tm->givenType)); @@ -186,7 +186,7 @@ TEST_CASE_FIXTURE(ClassFixture, "warn_when_prop_almost_matches") LUAU_REQUIRE_ERROR_COUNT(1, result); - auto err = get(result.errors[0]); + auto err = get(result.errors.at(0)); REQUIRE(err != nullptr); REQUIRE_EQ(1, err->candidates.size()); @@ -290,7 +290,7 @@ TEST_CASE_FIXTURE(ClassFixture, "table_properties_are_invariant") )"); LUAU_REQUIRE_ERROR_COUNT(2, result); - CHECK_EQ(6, result.errors[0].location.begin.line); + CHECK_EQ(6, result.errors.at(0).location.begin.line); CHECK_EQ(13, result.errors[1].location.begin.line); } @@ -313,7 +313,7 @@ TEST_CASE_FIXTURE(ClassFixture, "table_indexers_are_invariant") )"); LUAU_REQUIRE_ERROR_COUNT(2, result); - CHECK_EQ(6, result.errors[0].location.begin.line); + CHECK_EQ(6, result.errors.at(0).location.begin.line); CHECK_EQ(13, result.errors[1].location.begin.line); } @@ -331,7 +331,7 @@ TEST_CASE_FIXTURE(ClassFixture, "table_class_unification_reports_sane_errors_for )"); LUAU_REQUIRE_ERROR_COUNT(2, result); - REQUIRE_EQ("Key 'w' not found in class 'Vector2'", toString(result.errors[0])); + REQUIRE_EQ("Key 'w' not found in class 'Vector2'", toString(result.errors.at(0))); REQUIRE_EQ("Key 'x' not found in class 'Vector2'. Did you mean 'X'?", toString(result.errors[1])); } @@ -345,7 +345,7 @@ TEST_CASE_FIXTURE(ClassFixture, "class_unification_type_mismatch_is_correct_orde LUAU_REQUIRE_ERROR_COUNT(2, result); - REQUIRE_EQ("Type 'BaseClass' could not be converted into 'number'", toString(result.errors[0])); + REQUIRE_EQ("Type 'BaseClass' could not be converted into 'number'", toString(result.errors.at(0))); REQUIRE_EQ("Type 'number' could not be converted into 'BaseClass'", toString(result.errors[1])); } @@ -359,7 +359,7 @@ b.X = 2 -- real Vector2.X is also read-only )"); LUAU_REQUIRE_ERROR_COUNT(4, result); - CHECK_EQ("Value of type 'Vector2?' could be nil", toString(result.errors[0])); + CHECK_EQ("Value of type 'Vector2?' could be nil", toString(result.errors.at(0))); CHECK_EQ("Value of type 'Vector2?' could be nil", toString(result.errors[1])); CHECK_EQ("Key 'Z' not found in class 'Vector2'", toString(result.errors[2])); CHECK_EQ("Value of type 'Vector2?' could be nil", toString(result.errors[3])); @@ -385,7 +385,7 @@ b(a) caused by: Property 'Y' is not compatible. Type 'number' could not be converted into 'string')"; - CHECK_EQ(expected, toString(result.errors[0])); + CHECK_EQ(expected, toString(result.errors.at(0))); } TEST_CASE_FIXTURE(ClassFixture, "class_type_mismatch_with_name_conflict") @@ -397,7 +397,7 @@ local a: ChildClass = i )"); LUAU_REQUIRE_ERROR_COUNT(1, result); - CHECK_EQ("Type 'ChildClass' from 'Test' could not be converted into 'ChildClass' from 'MainModule'", toString(result.errors[0])); + CHECK_EQ("Type 'ChildClass' from 'Test' could not be converted into 'ChildClass' from 'MainModule'", toString(result.errors.at(0))); } TEST_CASE_FIXTURE(ClassFixture, "intersections_of_unions_of_classes") @@ -433,7 +433,7 @@ TEST_CASE_FIXTURE(ClassFixture, "index_instance_property") )"); LUAU_REQUIRE_ERROR_COUNT(1, result); - CHECK_EQ("Attempting a dynamic property access on type 'BaseClass' is unsafe and may cause exceptions at runtime", toString(result.errors[0])); + CHECK_EQ("Attempting a dynamic property access on type 'BaseClass' is unsafe and may cause exceptions at runtime", toString(result.errors.at(0))); } TEST_CASE_FIXTURE(ClassFixture, "index_instance_property_nonstrict") @@ -455,16 +455,22 @@ TEST_CASE_FIXTURE(ClassFixture, "type_mismatch_invariance_required_for_error") type A = { x: ChildClass } type B = { x: BaseClass } -local a: A +local a: A = { x = ChildClass.New() } local b: B = a )"); LUAU_REQUIRE_ERRORS(result); - const std::string expected = R"(Type 'A' could not be converted into 'B' + + if (FFlag::DebugLuauDeferredConstraintResolution) + CHECK(toString(result.errors.at(0)) == "Type 'a' could not be converted into 'B'; at [\"x\"], ChildClass is not exactly BaseClass"); + else + { + const std::string expected = R"(Type 'A' could not be converted into 'B' caused by: Property 'x' is not compatible. Type 'ChildClass' could not be converted into 'BaseClass' in an invariant context)"; - CHECK_EQ(expected, toString(result.errors[0])); + CHECK_EQ(expected, toString(result.errors.at(0))); + } } TEST_CASE_FIXTURE(ClassFixture, "callable_classes") @@ -551,7 +557,7 @@ TEST_CASE_FIXTURE(ClassFixture, "indexable_classes") CHECK_EQ( - toString(result.errors[0]), "Type 'boolean' could not be converted into 'number | string'; none of the union options are compatible"); + toString(result.errors.at(0)), "Type 'boolean' could not be converted into 'number | string'; none of the union options are compatible"); } { CheckResult result = check(R"( @@ -560,7 +566,7 @@ TEST_CASE_FIXTURE(ClassFixture, "indexable_classes") )"); CHECK_EQ( - toString(result.errors[0]), "Type 'boolean' could not be converted into 'number | string'; none of the union options are compatible"); + toString(result.errors.at(0)), "Type 'boolean' could not be converted into 'number | string'; none of the union options are compatible"); } // Test type checking for the return type of the indexer (i.e. a number) @@ -569,14 +575,14 @@ TEST_CASE_FIXTURE(ClassFixture, "indexable_classes") local x : IndexableClass x.key = "string value" )"); - CHECK_EQ(toString(result.errors[0]), "Type 'string' could not be converted into 'number'"); + CHECK_EQ(toString(result.errors.at(0)), "Type 'string' could not be converted into 'number'"); } { CheckResult result = check(R"( local x : IndexableClass local str : string = x.key )"); - CHECK_EQ(toString(result.errors[0]), "Type 'number' could not be converted into 'string'"); + CHECK_EQ(toString(result.errors.at(0)), "Type 'number' could not be converted into 'string'"); } // Check that we string key are rejected if the indexer's key type is not compatible with string @@ -593,9 +599,9 @@ TEST_CASE_FIXTURE(ClassFixture, "indexable_classes") x["key"] = 1 )"); if (FFlag::DebugLuauDeferredConstraintResolution) - CHECK_EQ(toString(result.errors[0]), "Key 'key' not found in class 'IndexableNumericKeyClass'"); + CHECK_EQ(toString(result.errors.at(0)), "Key 'key' not found in class 'IndexableNumericKeyClass'"); else - CHECK_EQ(toString(result.errors[0]), "Type 'string' could not be converted into 'number'"); + CHECK_EQ(toString(result.errors.at(0)), "Type 'string' could not be converted into 'number'"); } { CheckResult result = check(R"( @@ -603,14 +609,14 @@ TEST_CASE_FIXTURE(ClassFixture, "indexable_classes") local str : string x[str] = 1 -- Index with a non-const string )"); - CHECK_EQ(toString(result.errors[0]), "Type 'string' could not be converted into 'number'"); + CHECK_EQ(toString(result.errors.at(0)), "Type 'string' could not be converted into 'number'"); } { CheckResult result = check(R"( local x : IndexableNumericKeyClass local y = x.key )"); - CHECK_EQ(toString(result.errors[0]), "Key 'key' not found in class 'IndexableNumericKeyClass'"); + CHECK_EQ(toString(result.errors.at(0)), "Key 'key' not found in class 'IndexableNumericKeyClass'"); } { CheckResult result = check(R"( @@ -618,9 +624,9 @@ TEST_CASE_FIXTURE(ClassFixture, "indexable_classes") local y = x["key"] )"); if (FFlag::DebugLuauDeferredConstraintResolution) - CHECK_EQ(toString(result.errors[0]), "Key 'key' not found in class 'IndexableNumericKeyClass'"); + CHECK_EQ(toString(result.errors.at(0)), "Key 'key' not found in class 'IndexableNumericKeyClass'"); else - CHECK_EQ(toString(result.errors[0]), "Type 'string' could not be converted into 'number'"); + CHECK_EQ(toString(result.errors.at(0)), "Type 'string' could not be converted into 'number'"); } { CheckResult result = check(R"( @@ -628,7 +634,7 @@ TEST_CASE_FIXTURE(ClassFixture, "indexable_classes") local str : string local y = x[str] -- Index with a non-const string )"); - CHECK_EQ(toString(result.errors[0]), "Type 'string' could not be converted into 'number'"); + CHECK_EQ(toString(result.errors.at(0)), "Type 'string' could not be converted into 'number'"); } } diff --git a/tests/TypeInfer.generics.test.cpp b/tests/TypeInfer.generics.test.cpp index 1fbd9008..bff86b03 100644 --- a/tests/TypeInfer.generics.test.cpp +++ b/tests/TypeInfer.generics.test.cpp @@ -725,14 +725,21 @@ y.a.c = y )"); LUAU_REQUIRE_ERRORS(result); - const std::string expected = R"(Type 'y' could not be converted into 'T' + + if (FFlag::DebugLuauDeferredConstraintResolution) + CHECK(toString(result.errors.at(0)) == + R"(Type 'x' could not be converted into 'T'; type x["a"]["c"] (nil) is not exactly T["a"]["c"][0] (T))"); + else + { + const std::string expected = R"(Type 'y' could not be converted into 'T' caused by: Property 'a' is not compatible. Type '{ c: T?, d: number }' could not be converted into 'U' caused by: Property 'd' is not compatible. Type 'number' could not be converted into 'string' in an invariant context)"; - CHECK_EQ(expected, toString(result.errors[0])); + CHECK_EQ(expected, toString(result.errors[0])); + } } TEST_CASE_FIXTURE(Fixture, "generic_type_pack_unification1") diff --git a/tests/TypeInfer.intersectionTypes.test.cpp b/tests/TypeInfer.intersectionTypes.test.cpp index 6892c78f..2023a838 100644 --- a/tests/TypeInfer.intersectionTypes.test.cpp +++ b/tests/TypeInfer.intersectionTypes.test.cpp @@ -529,7 +529,7 @@ could not be converted into TEST_CASE_FIXTURE(Fixture, "intersection_of_tables_with_top_properties") { CheckResult result = check(R"( - local x : { p : number?, q : any } & { p : unknown, q : string? } + local x : { p : number?, q : any } & { p : unknown, q : string? } = { p = 123, q = "foo" } local y : { p : number?, q : string? } = x -- OK local z : { p : string?, q : number? } = x -- Not OK )"); diff --git a/tests/TypeInfer.modules.test.cpp b/tests/TypeInfer.modules.test.cpp index 3c126c7e..31f5f7f2 100644 --- a/tests/TypeInfer.modules.test.cpp +++ b/tests/TypeInfer.modules.test.cpp @@ -410,12 +410,18 @@ local b: B.T = a )"; CheckResult result = frontend.check("game/C"); - const std::string expected = R"(Type 'T' from 'game/A' could not be converted into 'T' from 'game/B' + LUAU_REQUIRE_ERROR_COUNT(1, result); + + if (FFlag::DebugLuauDeferredConstraintResolution) + CHECK(toString(result.errors.at(0)) == "Type 'a' could not be converted into 'T'; at [\"x\"], number is not exactly string"); + else + { + const std::string expected = R"(Type 'T' from 'game/A' could not be converted into 'T' from 'game/B' caused by: Property 'x' is not compatible. Type 'number' could not be converted into 'string' in an invariant context)"; - LUAU_REQUIRE_ERROR_COUNT(1, result); - CHECK_EQ(expected, toString(result.errors[0])); + CHECK_EQ(expected, toString(result.errors[0])); + } } TEST_CASE_FIXTURE(BuiltinsFixture, "module_type_conflict_instantiated") @@ -445,12 +451,18 @@ local b: B.T = a )"; CheckResult result = frontend.check("game/D"); - const std::string expected = R"(Type 'T' from 'game/B' could not be converted into 'T' from 'game/C' + LUAU_REQUIRE_ERROR_COUNT(1, result); + + if (FFlag::DebugLuauDeferredConstraintResolution) + CHECK(toString(result.errors.at(0)) == "Type 'a' could not be converted into 'T'; at [\"x\"], number is not exactly string"); + else + { + const std::string expected = R"(Type 'T' from 'game/B' could not be converted into 'T' from 'game/C' caused by: Property 'x' is not compatible. Type 'number' could not be converted into 'string' in an invariant context)"; - LUAU_REQUIRE_ERROR_COUNT(1, result); - CHECK_EQ(expected, toString(result.errors[0])); + CHECK_EQ(expected, toString(result.errors[0])); + } } TEST_CASE_FIXTURE(BuiltinsFixture, "constrained_anyification_clone_immutable_types") diff --git a/tests/TypeInfer.refinements.test.cpp b/tests/TypeInfer.refinements.test.cpp index cba1f37e..f0c70261 100644 --- a/tests/TypeInfer.refinements.test.cpp +++ b/tests/TypeInfer.refinements.test.cpp @@ -1939,8 +1939,17 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "refine_unknown_to_table") LUAU_REQUIRE_NO_ERRORS(result); - CHECK_EQ("unknown", toString(requireType("idx"))); - CHECK_EQ("unknown", toString(requireType("val"))); + if (FFlag::DebugLuauDeferredConstraintResolution) + { + // Bug: We do not simplify at the right time + CHECK_EQ("unknown?", toString(requireType("idx"))); + CHECK_EQ("unknown?", toString(requireType("val"))); + } + else + { + CHECK_EQ("unknown", toString(requireType("idx"))); + CHECK_EQ("unknown", toString(requireType("val"))); + } } TEST_CASE_FIXTURE(BuiltinsFixture, "conditional_refinement_should_stay_error_suppressing") diff --git a/tests/TypeInfer.singletons.test.cpp b/tests/TypeInfer.singletons.test.cpp index 1bc6b380..6fda6a2a 100644 --- a/tests/TypeInfer.singletons.test.cpp +++ b/tests/TypeInfer.singletons.test.cpp @@ -367,8 +367,8 @@ TEST_CASE_FIXTURE(Fixture, "parametric_tagged_union_alias") LUAU_REQUIRE_ERROR_COUNT(1, result); const std::string expectedError = - "Type 'a' could not be converted into 'Err | Ok'; type a (a) is not a subtype of Err | Ok[1] (Err)" - "\n\ttype a[\"success\"] (false) is not a subtype of Err | Ok[0][\"success\"] (true)"; + "Type 'a' could not be converted into 'Err | Ok'; type a (a) is not a subtype of Err | Ok[1] (Err)\n" + "\ttype a[\"success\"] (false) is not exactly Err | Ok[0][\"success\"] (true)"; CHECK(toString(result.errors[0]) == expectedError); } diff --git a/tests/TypeInfer.tables.test.cpp b/tests/TypeInfer.tables.test.cpp index 8e32a6a7..d3c15728 100644 --- a/tests/TypeInfer.tables.test.cpp +++ b/tests/TypeInfer.tables.test.cpp @@ -2147,16 +2147,22 @@ TEST_CASE_FIXTURE(Fixture, "error_detailed_prop") type A = { x: number, y: number } type B = { x: number, y: string } -local a: A +local a: A = { x = 123, y = 456 } local b: B = a )"); LUAU_REQUIRE_ERRORS(result); - const std::string expected = R"(Type 'A' could not be converted into 'B' + + if (FFlag::DebugLuauDeferredConstraintResolution) + CHECK(toString(result.errors.at(0)) == R"(Type 'a' could not be converted into 'B'; at ["y"], number is not exactly string)"); + else + { + const std::string expected = R"(Type 'A' could not be converted into 'B' caused by: Property 'y' is not compatible. Type 'number' could not be converted into 'string' in an invariant context)"; - CHECK_EQ(expected, toString(result.errors[0])); + CHECK_EQ(expected, toString(result.errors[0])); + } } TEST_CASE_FIXTURE(Fixture, "error_detailed_prop_nested") @@ -2168,19 +2174,25 @@ type BS = { x: number, y: string } type A = { a: boolean, b: AS } type B = { a: boolean, b: BS } -local a: A +local a: A = { a = false, b = { x = 123, y = 456 } } local b: B = a )"); LUAU_REQUIRE_ERRORS(result); - const std::string expected = R"(Type 'A' could not be converted into 'B' + + if (FFlag::DebugLuauDeferredConstraintResolution) + CHECK(toString(result.errors.at(0)) == R"(Type 'a' could not be converted into 'B'; at ["b"]["y"], number is not exactly string)"); + else + { + const std::string expected = R"(Type 'A' could not be converted into 'B' caused by: Property 'b' is not compatible. Type 'AS' could not be converted into 'BS' caused by: Property 'y' is not compatible. Type 'number' could not be converted into 'string' in an invariant context)"; - CHECK_EQ(expected, toString(result.errors[0])); + CHECK_EQ(expected, toString(result.errors[0])); + } } TEST_CASE_FIXTURE(BuiltinsFixture, "error_detailed_metatable_prop") @@ -3945,9 +3957,9 @@ TEST_CASE_FIXTURE(Fixture, "identify_all_problematic_table_fields") LUAU_REQUIRE_ERROR_COUNT(1, result); - std::string expected = "Type 'a' could not be converted into 'T'; at [\"a\"], string is not a subtype of number" - "\n\tat [\"b\"], boolean is not a subtype of string" - "\n\tat [\"c\"], number is not a subtype of boolean"; + std::string expected = "Type 'a' could not be converted into 'T'; at [\"a\"], string is not exactly number" + "\n\tat [\"b\"], boolean is not exactly string" + "\n\tat [\"c\"], number is not exactly boolean"; CHECK(toString(result.errors[0]) == expected); } diff --git a/tests/TypeInfer.test.cpp b/tests/TypeInfer.test.cpp index 5af34930..c34be936 100644 --- a/tests/TypeInfer.test.cpp +++ b/tests/TypeInfer.test.cpp @@ -28,8 +28,7 @@ TEST_CASE_FIXTURE(Fixture, "tc_hello_world") CheckResult result = check("local a = 7"); LUAU_REQUIRE_NO_ERRORS(result); - TypeId aType = requireType("a"); - CHECK_EQ(getPrimitiveType(aType), PrimitiveType::Number); + CHECK("number" == toString(requireType("a"))); } TEST_CASE_FIXTURE(Fixture, "tc_propagation") @@ -44,21 +43,39 @@ TEST_CASE_FIXTURE(Fixture, "tc_propagation") TEST_CASE_FIXTURE(Fixture, "tc_error") { CheckResult result = check("local a = 7 local b = 'hi' a = b"); - LUAU_REQUIRE_ERROR_COUNT(1, result); - CHECK_EQ( - result.errors[0], (TypeError{Location{Position{0, 35}, Position{0, 36}}, TypeMismatch{builtinTypes->numberType, builtinTypes->stringType}})); + if (FFlag::DebugLuauDeferredConstraintResolution) + { + LUAU_REQUIRE_NO_ERRORS(result); + CHECK("number | string" == toString(requireType("a"))); + } + else + { + LUAU_REQUIRE_ERROR_COUNT(1, result); + + CHECK_EQ( + result.errors[0], (TypeError{Location{Position{0, 35}, Position{0, 36}}, TypeMismatch{builtinTypes->numberType, builtinTypes->stringType}})); + } } TEST_CASE_FIXTURE(Fixture, "tc_error_2") { CheckResult result = check("local a = 7 a = 'hi'"); - LUAU_REQUIRE_ERROR_COUNT(1, result); - CHECK_EQ(result.errors[0], (TypeError{Location{Position{0, 18}, Position{0, 22}}, TypeMismatch{ - requireType("a"), - builtinTypes->stringType, - }})); + if (FFlag::DebugLuauDeferredConstraintResolution) + { + LUAU_REQUIRE_NO_ERRORS(result); + CHECK("number | string" == toString(requireType("a"))); + } + else + { + LUAU_REQUIRE_ERROR_COUNT(1, result); + + CHECK_EQ(result.errors[0], (TypeError{Location{Position{0, 18}, Position{0, 22}}, TypeMismatch{ + requireType("a"), + builtinTypes->stringType, + }})); + } } TEST_CASE_FIXTURE(Fixture, "infer_locals_with_nil_value") @@ -66,8 +83,15 @@ TEST_CASE_FIXTURE(Fixture, "infer_locals_with_nil_value") CheckResult result = check("local f = nil; f = 'hello world'"); LUAU_REQUIRE_NO_ERRORS(result); - TypeId ty = requireType("f"); - CHECK_EQ(getPrimitiveType(ty), PrimitiveType::String); + if (FFlag::DebugLuauDeferredConstraintResolution) + { + CHECK("string?" == toString(requireType("f"))); + } + else + { + TypeId ty = requireType("f"); + CHECK_EQ(getPrimitiveType(ty), PrimitiveType::String); + } } TEST_CASE_FIXTURE(Fixture, "infer_locals_with_nil_value_2") @@ -93,8 +117,8 @@ TEST_CASE_FIXTURE(Fixture, "infer_locals_via_assignment_from_its_call_site") if (FFlag::DebugLuauDeferredConstraintResolution) { - CHECK("number | string" == toString(requireType("a"))); - CHECK("(number | string) -> ()" == toString(requireType("f"))); + CHECK("unknown" == toString(requireType("a"))); + CHECK("(unknown) -> ()" == toString(requireType("f"))); LUAU_REQUIRE_NO_ERRORS(result); } @@ -105,27 +129,6 @@ TEST_CASE_FIXTURE(Fixture, "infer_locals_via_assignment_from_its_call_site") CHECK_EQ("number", toString(requireType("a"))); } } -TEST_CASE_FIXTURE(Fixture, "interesting_local_type_inference_case") -{ - if (!FFlag::DebugLuauDeferredConstraintResolution) - return; - - ScopedFastFlag sff[] = { - {"DebugLuauDeferredConstraintResolution", true}, - }; - - CheckResult result = check(R"( - local a - function f(x) a = x end - f({x = 5}) - f({x = 5}) - )"); - - CHECK("{ x: number }" == toString(requireType("a"))); - CHECK("({ x: number }) -> ()" == toString(requireType("f"))); - - LUAU_REQUIRE_NO_ERRORS(result); -} TEST_CASE_FIXTURE(Fixture, "infer_in_nocheck_mode") { @@ -178,8 +181,16 @@ TEST_CASE_FIXTURE(Fixture, "if_statement") LUAU_REQUIRE_NO_ERRORS(result); - CHECK_EQ(*builtinTypes->stringType, *requireType("a")); - CHECK_EQ(*builtinTypes->numberType, *requireType("b")); + if (FFlag::DebugLuauDeferredConstraintResolution) + { + CHECK("string?" == toString(requireType("a"))); + CHECK("number?" == toString(requireType("b"))); + } + else + { + CHECK_EQ(*builtinTypes->stringType, *requireType("a")); + CHECK_EQ(*builtinTypes->numberType, *requireType("b")); + } } TEST_CASE_FIXTURE(Fixture, "statements_are_topologically_sorted") diff --git a/tests/TypeInfer.typestates.test.cpp b/tests/TypeInfer.typestates.test.cpp index cee36832..2f07c148 100644 --- a/tests/TypeInfer.typestates.test.cpp +++ b/tests/TypeInfer.typestates.test.cpp @@ -274,4 +274,45 @@ TEST_CASE_FIXTURE(TypeStateFixture, "then_branch_assigns_but_is_met_with_return_ CHECK("string?" == toString(requireType("y"))); } +TEST_CASE_FIXTURE(TypeStateFixture, "invalidate_type_refinements_upon_assignments") +{ + CheckResult result = check(R"( + type Ok = { tag: "ok", val: T } + type Err = { tag: "err", err: E } + type Result = Ok | Err + + local function f(res: Result) + assert(res.tag == "ok") + local tag: "ok", val: T = res.tag, res.val + res = { tag = "err" :: "err", err = (5 :: any) :: E } + local tag: "err", err: E = res.tag, res.err + end + )"); + + LUAU_REQUIRE_NO_ERRORS(result); +} + +TEST_CASE_FIXTURE(TypeStateFixture, "local_t_is_assigned_a_fresh_table_with_x_assigned_a_union_and_then_assert_restricts_actual_outflow_of_types") +{ + CheckResult result = check(R"( + local t = nil + + if math.random() > 0.5 then + t = {} + t.x = if math.random() > 0.5 then 5 else "hello" + assert(typeof(t.x) == "string") + else + t = {} + t.x = if math.random() > 0.5 then 7 else true + assert(typeof(t.x) == "boolean") + end + + local x = t.x + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + // CHECK("boolean | string" == toString(requireType("x"))); + CHECK("boolean | number | number | string" == toString(requireType("x"))); +} + TEST_SUITE_END(); diff --git a/tests/conformance/buffers.lua b/tests/conformance/buffers.lua index 1cf996da..5da2a688 100644 --- a/tests/conformance/buffers.lua +++ b/tests/conformance/buffers.lua @@ -586,6 +586,15 @@ local function misc(t16) buffer.writei32(b, #t16, 10) assert(buffer.readi32(b, 16) == 10) + + buffer.writeu8(b, 100, 0xff) + buffer.writeu8(b, 110, 0x80) + assert(buffer.readu32(b, 100) == 255) + assert(buffer.readu32(b, 110) == 128) + buffer.writeu16(b, 200, 0xffff) + buffer.writeu16(b, 210, 0x8000) + assert(buffer.readu32(b, 200) == 65535) + assert(buffer.readu32(b, 210) == 32768) end misc(table.create(16, 0)) diff --git a/tests/conformance/native.lua b/tests/conformance/native.lua index 08d458f9..63c6ff09 100644 --- a/tests/conformance/native.lua +++ b/tests/conformance/native.lua @@ -275,4 +275,22 @@ end assert(arrayIndexingSpecialNumbers1(1, 256, 65536) == 3456789) +function loopIteratorProtocol(a, t) + local sum = 0 + + do + local a, b, c, d, e, f, g = {}, {}, {}, {}, {}, {}, {} + end + + for k, v in ipairs(t) do + if k == 10 then sum += math.abs('-8') end + + sum += k + end + + return sum +end + +assert(loopIteratorProtocol(0, table.create(100, 5)) == 5058) + return('OK') diff --git a/tools/faillist.txt b/tools/faillist.txt index c421f2ce..61fb4f47 100644 --- a/tools/faillist.txt +++ b/tools/faillist.txt @@ -13,7 +13,6 @@ AutocompleteTest.autocomplete_string_singleton_equality AutocompleteTest.autocomplete_string_singleton_escape AutocompleteTest.autocomplete_string_singletons AutocompleteTest.do_wrong_compatible_nonself_calls -AutocompleteTest.frontend_use_correct_global_scope AutocompleteTest.no_incompatible_self_calls_on_class AutocompleteTest.string_singleton_in_if_statement AutocompleteTest.suggest_external_module_type @@ -165,7 +164,6 @@ GenericsTests.generic_argument_count_too_many GenericsTests.generic_factories GenericsTests.generic_functions_dont_cache_type_parameters GenericsTests.generic_functions_in_types -GenericsTests.generic_functions_should_be_memory_safe GenericsTests.generic_type_pack_parentheses GenericsTests.generic_type_pack_unification1 GenericsTests.generic_type_pack_unification2 @@ -234,7 +232,6 @@ Linter.DeprecatedApiFenv Linter.FormatStringTyped Linter.TableOperationsIndexer ModuleTests.clone_self_property -Negations.cofinite_strings_can_be_compared_for_equality Negations.negated_string_is_a_subtype_of_string NonstrictModeTests.inconsistent_module_return_types_are_ok NonstrictModeTests.infer_nullary_function @@ -286,8 +283,6 @@ RefinementTest.function_call_with_colon_after_refining_not_to_be_nil RefinementTest.impossible_type_narrow_is_not_an_error RefinementTest.index_on_a_refined_property RefinementTest.isa_type_refinement_must_be_known_ahead_of_time -RefinementTest.luau_polyfill_isindexkey_refine_conjunction -RefinementTest.luau_polyfill_isindexkey_refine_conjunction_variant RefinementTest.merge_should_be_fully_agnostic_of_hashmap_ordering RefinementTest.narrow_property_of_a_bounded_variable RefinementTest.nonoptional_type_can_narrow_to_nil_if_sense_is_true @@ -340,15 +335,12 @@ TableTests.dont_suggest_exact_match_keys TableTests.error_detailed_indexer_key TableTests.error_detailed_indexer_value TableTests.error_detailed_metatable_prop -TableTests.error_detailed_prop -TableTests.error_detailed_prop_nested TableTests.expected_indexer_from_table_union TableTests.expected_indexer_value_type_extra TableTests.expected_indexer_value_type_extra_2 TableTests.explicitly_typed_table TableTests.explicitly_typed_table_error TableTests.explicitly_typed_table_with_indexer -TableTests.fuzz_table_unify_instantiated_table_with_prop_realloc TableTests.generalize_table_argument TableTests.generic_table_instantiation_potential_regression TableTests.indexer_mismatch @@ -420,7 +412,6 @@ TableTests.table_unifies_into_map TableTests.top_table_type TableTests.type_mismatch_on_massive_table_is_cut_short TableTests.unification_of_unions_in_a_self_referential_type -TableTests.unifying_tables_shouldnt_uaf1 TableTests.used_colon_instead_of_dot TableTests.used_dot_instead_of_colon TableTests.used_dot_instead_of_colon_but_correctly @@ -485,14 +476,10 @@ TypeInfer.follow_on_new_types_in_substitution TypeInfer.globals TypeInfer.globals2 TypeInfer.globals_are_banned_in_strict_mode -TypeInfer.if_statement TypeInfer.infer_assignment_value_types -TypeInfer.infer_assignment_value_types_mutable_lval TypeInfer.infer_locals_via_assignment_from_its_call_site -TypeInfer.infer_locals_with_nil_value TypeInfer.infer_through_group_expr TypeInfer.infer_type_assertion_value_type -TypeInfer.interesting_local_type_inference_case TypeInfer.no_infinite_loop_when_trying_to_unify_uh_this TypeInfer.no_stack_overflow_from_isoptional TypeInfer.promote_tail_type_packs @@ -500,8 +487,6 @@ TypeInfer.recursive_function_that_invokes_itself_with_a_refinement_of_its_parame TypeInfer.recursive_function_that_invokes_itself_with_a_refinement_of_its_parameter_2 TypeInfer.stringify_nested_unions_with_optionals TypeInfer.tc_after_error_recovery_no_replacement_name_in_error -TypeInfer.tc_error -TypeInfer.tc_error_2 TypeInfer.tc_if_else_expressions_expected_type_3 TypeInfer.type_infer_recursion_limit_no_ice TypeInfer.type_infer_recursion_limit_normalizer @@ -531,7 +516,6 @@ TypeInferClasses.intersections_of_unions_of_classes TypeInferClasses.optional_class_field_access_error TypeInferClasses.table_class_unification_reports_sane_errors_for_missing_properties TypeInferClasses.table_indexers_are_invariant -TypeInferClasses.type_mismatch_invariance_required_for_error TypeInferClasses.unions_of_intersections_of_classes TypeInferClasses.we_can_report_when_someone_is_trying_to_use_a_table_rather_than_a_class TypeInferFunctions.another_other_higher_order_function @@ -605,6 +589,7 @@ TypeInferLoops.for_in_loop_on_non_function TypeInferLoops.for_in_loop_with_custom_iterator TypeInferLoops.for_in_loop_with_incompatible_args_to_iterator TypeInferLoops.for_in_loop_with_next +TypeInferLoops.for_in_with_a_custom_iterator_should_type_check TypeInferLoops.for_in_with_an_iterator_of_type_any TypeInferLoops.for_in_with_generic_next TypeInferLoops.for_in_with_just_one_iterator_is_ok @@ -626,10 +611,7 @@ TypeInferLoops.varlist_declared_by_for_in_loop_should_be_free TypeInferLoops.while_loop TypeInferModules.bound_free_table_export_is_ok TypeInferModules.do_not_modify_imported_types -TypeInferModules.do_not_modify_imported_types_4 TypeInferModules.do_not_modify_imported_types_5 -TypeInferModules.module_type_conflict -TypeInferModules.module_type_conflict_instantiated TypeInferModules.require TypeInferModules.require_failed_module TypeInferOOP.CheckMethodsOfSealed @@ -680,7 +662,6 @@ TypeInferUnknownNever.index_on_union_of_tables_for_properties_that_is_sorta_neve TypeInferUnknownNever.length_of_never TypeInferUnknownNever.math_operators_and_never TypeInferUnknownNever.type_packs_containing_never_is_itself_uninhabitable -TypePackTests.fuzz_typepack_iter_follow_2 TypePackTests.pack_tail_unification_check TypePackTests.type_alias_backwards_compatible TypePackTests.type_alias_default_type_errors @@ -699,6 +680,8 @@ TypeSingletons.table_properties_singleton_strings TypeSingletons.table_properties_type_error_escapes TypeSingletons.widen_the_supertype_if_it_is_free_and_subtype_has_singleton TypeSingletons.widening_happens_almost_everywhere +TypeStatesTest.invalidate_type_refinements_upon_assignments +TypeStatesTest.local_t_is_assigned_a_fresh_table_with_x_assigned_a_union_and_then_assert_restricts_actual_outflow_of_types UnionTypes.disallow_less_specific_assign UnionTypes.disallow_less_specific_assign2 UnionTypes.error_detailed_optional diff --git a/stats/compiler-stats.py b/tools/heuristicstat.py similarity index 100% rename from stats/compiler-stats.py rename to tools/heuristicstat.py diff --git a/tools/test_dcr.py b/tools/test_dcr.py index 30f8a310..3598b02c 100644 --- a/tools/test_dcr.py +++ b/tools/test_dcr.py @@ -113,6 +113,12 @@ def main(): action="store_true", help="Run the tests with read-write properties enabled.", ) + parser.add_argument( + "--ts", + dest="suite", + action="store", + help="Only run a specific suite." + ) parser.add_argument("--randomize", action="store_true", help="Pick a random seed") @@ -139,6 +145,9 @@ def main(): elif args.randomize: commandLine.append("--randomize") + if args.suite: + commandLine.append(f'--ts={args.suite}') + print_stderr(">", " ".join(commandLine)) p = sp.Popen( @@ -146,6 +155,8 @@ def main(): stdout=sp.PIPE, ) + assert p.stdout + handler = Handler(failList) if args.dump: