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