diff --git a/Analysis/include/Luau/Constraint.h b/Analysis/include/Luau/Constraint.h index 38506f0a..5f9c4572 100644 --- a/Analysis/include/Luau/Constraint.h +++ b/Analysis/include/Luau/Constraint.h @@ -14,8 +14,16 @@ namespace Luau { +enum class ValueContext; struct Scope; +// if resultType is a freeType, assignmentType <: freeType <: resultType bounds +struct EqualityConstraint +{ + TypeId resultType; + TypeId assignmentType; +}; + // subType <: superType struct SubtypeConstraint { @@ -40,6 +48,8 @@ struct GeneralizationConstraint { TypeId generalizedType; TypeId sourceType; + + std::vector interiorTypes; }; // subType ~ inst superType @@ -145,6 +155,7 @@ struct HasPropConstraint TypeId resultType; TypeId subjectType; std::string prop; + ValueContext context; // HACK: We presently need types like true|false or string|"hello" when // deciding whether a particular literal expression should have a singleton @@ -256,7 +267,8 @@ struct ReducePackConstraint using ConstraintV = Variant; + SetPropConstraint, SetIndexerConstraint, SingletonOrTopTypeConstraint, UnpackConstraint, SetOpConstraint, ReduceConstraint, ReducePackConstraint, + EqualityConstraint>; struct Constraint { diff --git a/Analysis/include/Luau/ConstraintGenerator.h b/Analysis/include/Luau/ConstraintGenerator.h index 65318a31..3d3dcb4d 100644 --- a/Analysis/include/Luau/ConstraintGenerator.h +++ b/Analysis/include/Luau/ConstraintGenerator.h @@ -130,6 +130,8 @@ struct ConstraintGenerator void visitModuleRoot(AstStatBlock* block); private: + std::vector> interiorTypes; + /** * Fabricates a new free type belonging to a given scope. * @param scope the scope the free type belongs to. diff --git a/Analysis/include/Luau/ConstraintSolver.h b/Analysis/include/Luau/ConstraintSolver.h index 0f4be214..54d229c3 100644 --- a/Analysis/include/Luau/ConstraintSolver.h +++ b/Analysis/include/Luau/ConstraintSolver.h @@ -8,6 +8,7 @@ #include "Luau/Location.h" #include "Luau/Module.h" #include "Luau/Normalize.h" +#include "Luau/Substitution.h" #include "Luau/ToString.h" #include "Luau/Type.h" #include "Luau/TypeCheckLimits.h" @@ -20,6 +21,8 @@ namespace Luau { +enum class ValueContext; + struct DcrLogger; // TypeId, TypePackId, or Constraint*. It is impossible to know which, but we @@ -110,8 +113,6 @@ struct ConstraintSolver bool isDone(); - void finalizeModule(); - /** Attempt to dispatch a constraint. Returns true if it was successful. If * tryDispatch() returns false, the constraint remains in the unsolved set * and will be retried later. @@ -136,6 +137,7 @@ struct ConstraintSolver bool tryDispatch(const SetOpConstraint& c, NotNull constraint, bool force); bool tryDispatch(const ReduceConstraint& c, NotNull constraint, bool force); bool tryDispatch(const ReducePackConstraint& c, NotNull constraint, bool force); + bool tryDispatch(const EqualityConstraint& c, NotNull constraint, bool force); // for a, ... in some_table do // also handles __iter metamethod @@ -146,9 +148,9 @@ struct ConstraintSolver TypeId nextTy, TypeId tableTy, TypeId firstIndexTy, const IterableConstraint& c, NotNull constraint, bool force); std::pair, std::optional> lookupTableProp( - TypeId subjectType, const std::string& propName, bool suppressSimplification = false); + TypeId subjectType, const std::string& propName, ValueContext context, bool suppressSimplification = false); std::pair, std::optional> lookupTableProp( - TypeId subjectType, const std::string& propName, bool suppressSimplification, DenseHashSet& seen); + TypeId subjectType, const std::string& propName, ValueContext context, bool suppressSimplification, DenseHashSet& seen); void block(NotNull target, NotNull constraint); /** @@ -208,23 +210,6 @@ struct ConstraintSolver */ bool isBlocked(NotNull constraint); - /** - * Creates a new Unifier and performs a single unification operation. Commits - * the result. - * @param subType the sub-type to unify. - * @param superType the super-type to unify. - * @returns optionally a unification too complex error if unification failed - */ - std::optional unify(NotNull scope, Location location, TypeId subType, TypeId superType); - - /** - * Creates a new Unifier and performs a single unification operation. Commits - * the result. - * @param subPack the sub-type pack to unify. - * @param superPack the super-type pack to unify. - */ - ErrorVec unify(NotNull scope, Location location, TypePackId subPack, TypePackId superPack); - /** Pushes a new solver constraint to the solver. * @param cv the body of the constraint. **/ @@ -253,20 +238,33 @@ struct ConstraintSolver */ bool hasUnresolvedConstraints(TypeId ty); -private: - /** Helper used by tryDispatch(SubtypeConstraint) and - * tryDispatch(PackSubtypeConstraint) + /** + * Creates a new Unifier and performs a single unification operation. * - * Attempts to unify subTy with superTy. If doing so would require unifying + * @param subType the sub-type to unify. + * @param superType the super-type to unify. + * @returns true if the unification succeeded. False if the unification was + * too complex. + */ + template + bool unify(NotNull scope, Location location, TID subType, TID superType); + + /** Attempts to unify subTy with superTy. If doing so would require unifying * BlockedTypes, fail and block the constraint on those BlockedTypes. * + * Note: TID can only be TypeId or TypePackId. + * * If unification fails, replace all free types with errorType. * * If unification succeeds, unblock every type changed by the unification. + * + * @returns true if the unification succeeded. False if the unification was + * too complex. */ template - bool tryUnify(NotNull constraint, TID subTy, TID superTy); + bool unify(NotNull constraint, TID subTy, TID superTy); +private: /** * Bind a BlockedType to another type while taking care not to bind it to * itself in the case that resultTy == blockedTy. This can happen if we @@ -295,6 +293,13 @@ private: **/ void unblock_(BlockedConstraintId progressed); + /** + * Reproduces any constraints necessary for new types that are copied when applying a substitution. + * At the time of writing, this pertains only to type families. + * @param subst the substitution that was applied + **/ + void reproduceConstraints(NotNull scope, const Location& location, const Substitution& subst); + TypeId errorRecoveryType() const; TypePackId errorRecoveryTypePack() const; diff --git a/Analysis/include/Luau/Error.h b/Analysis/include/Luau/Error.h index 5a2caf65..d9b5f1ba 100644 --- a/Analysis/include/Luau/Error.h +++ b/Analysis/include/Luau/Error.h @@ -380,6 +380,20 @@ struct NonStrictFunctionDefinitionError bool operator==(const NonStrictFunctionDefinitionError& rhs) const; }; +struct PropertyAccessViolation +{ + TypeId table; + Name key; + + enum + { + CannotRead, + CannotWrite + } context; + + bool operator==(const PropertyAccessViolation& rhs) const; +}; + struct CheckedFunctionIncorrectArgs { std::string functionName; @@ -388,14 +402,28 @@ struct CheckedFunctionIncorrectArgs bool operator==(const CheckedFunctionIncorrectArgs& rhs) const; }; -using TypeErrorData = - Variant; +struct UnexpectedTypeInSubtyping +{ + TypeId ty; + + bool operator==(const UnexpectedTypeInSubtyping& rhs) const; +}; + +struct UnexpectedTypePackInSubtyping +{ + TypePackId tp; + + bool operator==(const UnexpectedTypePackInSubtyping& rhs) const; +}; + +using TypeErrorData = Variant; struct TypeErrorSummary { diff --git a/Analysis/include/Luau/Instantiation2.h b/Analysis/include/Luau/Instantiation2.h new file mode 100644 index 00000000..9dfbb613 --- /dev/null +++ b/Analysis/include/Luau/Instantiation2.h @@ -0,0 +1,70 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#pragma once + +#include "Luau/NotNull.h" +#include "Luau/Substitution.h" +#include "Luau/TxnLog.h" +#include "Luau/TypeFwd.h" +#include "Luau/Unifiable.h" + +namespace Luau +{ + +struct TypeArena; +struct TypeCheckLimits; + +struct Replacer : Substitution +{ + DenseHashMap replacements; + DenseHashMap replacementPacks; + + Replacer(NotNull arena, DenseHashMap replacements, DenseHashMap replacementPacks) + : Substitution(TxnLog::empty(), arena) + , replacements(std::move(replacements)) + , replacementPacks(std::move(replacementPacks)) + { + } + + bool isDirty(TypeId ty) override + { + return replacements.find(ty) != nullptr; + } + + bool isDirty(TypePackId tp) override + { + return replacementPacks.find(tp) != nullptr; + } + + TypeId clean(TypeId ty) override + { + return replacements[ty]; + } + + TypePackId clean(TypePackId tp) override + { + return replacementPacks[tp]; + } +}; + +// A substitution which replaces generic functions by monomorphic functions +struct Instantiation2 : Substitution +{ + // Mapping from generic types to free types to be used in instantiation. + DenseHashMap genericSubstitutions{nullptr}; + // Mapping from generic type packs to `TypePack`s of free types to be used in instantiation. + DenseHashMap genericPackSubstitutions{nullptr}; + + Instantiation2(TypeArena* arena, DenseHashMap genericSubstitutions, DenseHashMap genericPackSubstitutions) + : Substitution(TxnLog::empty(), arena) + , genericSubstitutions(std::move(genericSubstitutions)) + , genericPackSubstitutions(std::move(genericPackSubstitutions)) + { + } + + bool isDirty(TypeId ty) override; + bool isDirty(TypePackId tp) override; + TypeId clean(TypeId ty) override; + TypePackId clean(TypePackId tp) override; +}; + +} // namespace Luau diff --git a/Analysis/include/Luau/Subtyping.h b/Analysis/include/Luau/Subtyping.h index 0ca2380e..8fa4b8b0 100644 --- a/Analysis/include/Luau/Subtyping.h +++ b/Analysis/include/Luau/Subtyping.h @@ -21,14 +21,15 @@ struct InternalErrorReporter; class TypeIds; class Normalizer; -struct NormalizedType; struct NormalizedClassType; -struct NormalizedStringType; struct NormalizedFunctionType; -struct TypeArena; -struct TypeCheckLimits; +struct NormalizedStringType; +struct NormalizedType; +struct Property; struct Scope; struct TableIndexer; +struct TypeArena; +struct TypeCheckLimits; enum class SubtypingVariance { @@ -79,6 +80,7 @@ struct SubtypingResult SubtypingResult& withSubPath(TypePath::Path path); SubtypingResult& withSuperPath(TypePath::Path path); SubtypingResult& withErrors(ErrorVec& err); + SubtypingResult& withError(TypeError err); // Only negates the `isSubtype`. static SubtypingResult negate(const SubtypingResult& result); @@ -102,6 +104,10 @@ struct SubtypingEnvironment DenseHashMap mappedGenericPacks{nullptr}; DenseHashMap, SubtypingResult, TypePairHash> ephemeralCache{{}}; + + /// Applies `mappedGenerics` to the given type. + /// This is used specifically to substitute for generics in type family instances. + std::optional applyMappedGenerics(NotNull builtinTypes, NotNull arena, TypeId ty); }; struct Subtyping @@ -192,6 +198,7 @@ private: SubtypingResult isCovariantWith(SubtypingEnvironment& env, const SingletonType* subSingleton, const TableType* superTable); SubtypingResult isCovariantWith(SubtypingEnvironment& env, const TableIndexer& subIndexer, const TableIndexer& superIndexer); + SubtypingResult isCovariantWith(SubtypingEnvironment& env, const Property& subProperty, const Property& superProperty, const std::string& name); SubtypingResult isCovariantWith(SubtypingEnvironment& env, const NormalizedType* subNorm, const NormalizedType* superNorm); SubtypingResult isCovariantWith(SubtypingEnvironment& env, const NormalizedClassType& subClass, const NormalizedClassType& superClass); diff --git a/Analysis/include/Luau/Type.h b/Analysis/include/Luau/Type.h index c72864ce..a0c6b9cc 100644 --- a/Analysis/include/Luau/Type.h +++ b/Analysis/include/Luau/Type.h @@ -414,16 +414,11 @@ struct Property TypeId type() const; void setType(TypeId ty); - // Should only be called in RWP! - // We do not assert that `readTy` nor `writeTy` are nullopt or not. - // The invariant is that at least one of them mustn't be nullopt, which we do assert here. - // TODO: Kill this in favor of exposing `readTy`/`writeTy` directly? If we do, we'll lose the asserts which will be useful while debugging. - std::optional readType() const; - std::optional writeType() const; - bool isShared() const; + bool isReadOnly() const; + bool isWriteOnly() const; + bool isReadWrite() const; -private: std::optional readTy; std::optional writeTy; }; @@ -844,6 +839,7 @@ public: const TypePackId emptyTypePack; const TypePackId anyTypePack; + const TypePackId unknownTypePack; const TypePackId neverTypePack; const TypePackId uninhabitableTypePack; const TypePackId errorTypePack; diff --git a/Analysis/include/Luau/TypeFamily.h b/Analysis/include/Luau/TypeFamily.h index 79dc435c..389a2e00 100644 --- a/Analysis/include/Luau/TypeFamily.h +++ b/Analysis/include/Luau/TypeFamily.h @@ -30,8 +30,10 @@ struct TypeFamilyContext // nullptr if the type family is being reduced outside of the constraint solver. ConstraintSolver* solver; + // The constraint being reduced in this run of the reduction + const Constraint* constraint; - TypeFamilyContext(NotNull cs, NotNull scope) + TypeFamilyContext(NotNull cs, NotNull scope, NotNull constraint) : arena(cs->arena) , builtins(cs->builtinTypes) , scope(scope) @@ -39,6 +41,7 @@ struct TypeFamilyContext , ice(NotNull{&cs->iceReporter}) , limits(NotNull{&cs->limits}) , solver(cs.get()) + , constraint(constraint.get()) { } @@ -51,10 +54,10 @@ struct TypeFamilyContext , ice(ice) , limits(limits) , solver(nullptr) + , constraint(nullptr) { } }; - /// Represents a reduction result, which may have successfully reduced the type, /// may have concretely failed to reduce the type, or may simply be stuck /// without more information. diff --git a/Analysis/include/Luau/TypeUtils.h b/Analysis/include/Luau/TypeUtils.h index 6ac4e7c6..2fcc8fef 100644 --- a/Analysis/include/Luau/TypeUtils.h +++ b/Analysis/include/Luau/TypeUtils.h @@ -28,6 +28,8 @@ std::optional findMetatableEntry( NotNull builtinTypes, ErrorVec& errors, TypeId type, const std::string& entry, Location location); std::optional findTablePropertyRespectingMeta( NotNull builtinTypes, ErrorVec& errors, TypeId ty, const std::string& name, Location location); +std::optional findTablePropertyRespectingMeta( + NotNull builtinTypes, ErrorVec& errors, TypeId ty, const std::string& name, ValueContext context, Location location); // Returns the minimum and maximum number of types the argument list can accept. std::pair> getParameterExtents(const TxnLog* log, TypePackId tp, bool includeHiddenVariadics = false); diff --git a/Analysis/include/Luau/Unifier2.h b/Analysis/include/Luau/Unifier2.h index f9a3fdc9..a0553a2a 100644 --- a/Analysis/include/Luau/Unifier2.h +++ b/Analysis/include/Luau/Unifier2.h @@ -38,6 +38,11 @@ struct Unifier2 DenseHashMap> expandedFreeTypes{nullptr}; + // Mapping from generic types to free types to be used in instantiation. + DenseHashMap genericSubstitutions{nullptr}; + // Mapping from generic type packs to `TypePack`s of free types to be used in instantiation. + DenseHashMap genericPackSubstitutions{nullptr}; + int recursionCount = 0; int recursionLimit = 0; diff --git a/Analysis/include/Luau/VisitType.h b/Analysis/include/Luau/VisitType.h index 6e1fea6a..94510e32 100644 --- a/Analysis/include/Luau/VisitType.h +++ b/Analysis/include/Luau/VisitType.h @@ -11,7 +11,6 @@ LUAU_FASTINT(LuauVisitRecursionLimit) LUAU_FASTFLAG(LuauBoundLazyTypes2) LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution) -LUAU_FASTFLAG(DebugLuauReadWriteProperties) namespace Luau { @@ -281,12 +280,16 @@ struct GenericTypeVisitor { for (auto& [_name, prop] : ttv->props) { - if (FFlag::DebugLuauReadWriteProperties) + if (FFlag::DebugLuauDeferredConstraintResolution) { - if (auto ty = prop.readType()) + if (auto ty = prop.readTy) traverse(*ty); - if (auto ty = prop.writeType()) + // In the case that the readType and the writeType + // are the same pointer, just traverse once. + // Traversing each property twice has pretty + // significant performance consequences. + if (auto ty = prop.writeTy; ty && !prop.isShared()) traverse(*ty); } else @@ -315,12 +318,16 @@ struct GenericTypeVisitor { for (const auto& [name, prop] : ctv->props) { - if (FFlag::DebugLuauReadWriteProperties) + if (FFlag::DebugLuauDeferredConstraintResolution) { - if (auto ty = prop.readType()) + if (auto ty = prop.readTy) traverse(*ty); - if (auto ty = prop.writeType()) + // In the case that the readType and the writeType are + // the same pointer, just traverse once. Traversing each + // property twice would have pretty significant + // performance consequences. + if (auto ty = prop.writeTy; ty && !prop.isShared()) traverse(*ty); } else diff --git a/Analysis/src/AstQuery.cpp b/Analysis/src/AstQuery.cpp index cea81080..cebb226a 100644 --- a/Analysis/src/AstQuery.cpp +++ b/Analysis/src/AstQuery.cpp @@ -11,7 +11,7 @@ #include -LUAU_FASTFLAG(DebugLuauReadWriteProperties) +LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution); namespace Luau { @@ -524,9 +524,9 @@ std::optional getDocumentationSymbolAtPosition(const Source { if (auto propIt = ttv->props.find(indexName->index.value); propIt != ttv->props.end()) { - if (FFlag::DebugLuauReadWriteProperties) + if (FFlag::DebugLuauDeferredConstraintResolution) { - if (auto ty = propIt->second.readType()) + if (auto ty = propIt->second.readTy) return checkOverloadedDocumentationSymbol(module, *ty, parentExpr, propIt->second.documentationSymbol); } else @@ -537,9 +537,9 @@ std::optional getDocumentationSymbolAtPosition(const Source { if (auto propIt = ctv->props.find(indexName->index.value); propIt != ctv->props.end()) { - if (FFlag::DebugLuauReadWriteProperties) + if (FFlag::DebugLuauDeferredConstraintResolution) { - if (auto ty = propIt->second.readType()) + if (auto ty = propIt->second.readTy) return checkOverloadedDocumentationSymbol(module, *ty, parentExpr, propIt->second.documentationSymbol); } else diff --git a/Analysis/src/Autocomplete.cpp b/Analysis/src/Autocomplete.cpp index 1c8620b8..d5c947da 100644 --- a/Analysis/src/Autocomplete.cpp +++ b/Analysis/src/Autocomplete.cpp @@ -14,7 +14,6 @@ #include LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution); -LUAU_FASTFLAG(DebugLuauReadWriteProperties); LUAU_FASTFLAGVARIABLE(LuauAutocompleteStringLiteralBounds, false); LUAU_FASTFLAGVARIABLE(LuauAutocompleteTableKeysNoInitialCharacter, false); @@ -277,9 +276,9 @@ static void autocompleteProps(const Module& module, TypeArena* typeArena, NotNul { Luau::TypeId type; - if (FFlag::DebugLuauReadWriteProperties) + if (FFlag::DebugLuauDeferredConstraintResolution) { - if (auto ty = prop.readType()) + if (auto ty = prop.readTy) type = follow(*ty); else continue; diff --git a/Analysis/src/BuiltinDefinitions.cpp b/Analysis/src/BuiltinDefinitions.cpp index c0d88d9a..a8b17c55 100644 --- a/Analysis/src/BuiltinDefinitions.cpp +++ b/Analysis/src/BuiltinDefinitions.cpp @@ -283,6 +283,26 @@ void registerBuiltinGlobals(Frontend& frontend, GlobalTypes& globals, bool typeC } attachMagicFunction(getGlobalBinding(globals, "assert"), magicFunctionAssert); + + if (FFlag::DebugLuauDeferredConstraintResolution) + { + // declare function assert(value: T, errorMessage: string?): intersect + TypeId genericT = arena.addType(GenericType{"T"}); + TypeId refinedTy = arena.addType(TypeFamilyInstanceType{ + NotNull{&kBuiltinTypeFamilies.intersectFamily}, + {genericT, arena.addType(NegationType{builtinTypes->falsyType})}, + {} + }); + + TypeId assertTy = arena.addType(FunctionType{ + {genericT}, + {}, + arena.addTypePack(TypePack{{genericT, builtinTypes->optionalStringType}}), + arena.addTypePack(TypePack{{refinedTy}}) + }); + addGlobalBinding(globals, "assert", assertTy, "@luau"); + } + attachMagicFunction(getGlobalBinding(globals, "setmetatable"), magicFunctionSetMetaTable); attachMagicFunction(getGlobalBinding(globals, "select"), magicFunctionSelect); attachDcrMagicFunction(getGlobalBinding(globals, "select"), dcrMagicFunctionSelect); diff --git a/Analysis/src/Clone.cpp b/Analysis/src/Clone.cpp index 6d2d04b6..bf02f743 100644 --- a/Analysis/src/Clone.cpp +++ b/Analysis/src/Clone.cpp @@ -8,8 +8,6 @@ #include "Luau/TypePack.h" #include "Luau/Unifiable.h" -LUAU_FASTFLAG(DebugLuauReadWriteProperties) - LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution) LUAU_FASTINTVARIABLE(LuauTypeCloneRecursionLimit, 300) @@ -196,14 +194,14 @@ private: Property shallowClone(const Property& p) { - if (FFlag::DebugLuauReadWriteProperties) + if (FFlag::DebugLuauDeferredConstraintResolution) { std::optional cloneReadTy; - if (auto ty = p.readType()) + if (auto ty = p.readTy) cloneReadTy = shallowClone(*ty); std::optional cloneWriteTy; - if (auto ty = p.writeType()) + if (auto ty = p.writeTy) cloneWriteTy = shallowClone(*ty); std::optional cloned = Property::create(cloneReadTy, cloneWriteTy); @@ -460,14 +458,14 @@ namespace Property clone(const Property& prop, TypeArena& dest, CloneState& cloneState) { - if (FFlag::DebugLuauReadWriteProperties) + if (FFlag::DebugLuauDeferredConstraintResolution) { std::optional cloneReadTy; - if (auto ty = prop.readType()) + if (auto ty = prop.readTy) cloneReadTy = clone(*ty, dest, cloneState); std::optional cloneWriteTy; - if (auto ty = prop.writeType()) + if (auto ty = prop.writeTy) cloneWriteTy = clone(*ty, dest, cloneState); std::optional cloned = Property::create(cloneReadTy, cloneWriteTy); diff --git a/Analysis/src/ConstraintGenerator.cpp b/Analysis/src/ConstraintGenerator.cpp index 6f744951..17bffe87 100644 --- a/Analysis/src/ConstraintGenerator.cpp +++ b/Analysis/src/ConstraintGenerator.cpp @@ -217,9 +217,25 @@ void ConstraintGenerator::visitModuleRoot(AstStatBlock* block) rootScope->returnType = freshTypePack(scope); + TypeId moduleFnTy = arena->addType(FunctionType{TypeLevel{}, rootScope, builtinTypes->anyTypePack, rootScope->returnType}); + interiorTypes.emplace_back(); + prepopulateGlobalScope(scope, block); - visitBlockWithoutChildScope(scope, block); + Checkpoint start = checkpoint(this); + + ControlFlow cf = visitBlockWithoutChildScope(scope, block); + if (cf == ControlFlow::None) + addConstraint(scope, block->location, PackSubtypeConstraint{builtinTypes->emptyTypePack, rootScope->returnType}); + + Checkpoint end = checkpoint(this); + + NotNull genConstraint = addConstraint(scope, block->location, GeneralizationConstraint{arena->addType(BlockedType{}), moduleFnTy, std::move(interiorTypes.back())}); + forEachConstraint(start, end, this, [genConstraint](const ConstraintPtr& c) { + genConstraint->dependencies.push_back(NotNull{c.get()}); + }); + + interiorTypes.pop_back(); fillInInferredBindings(scope, block); @@ -406,7 +422,7 @@ void ConstraintGenerator::computeRefinement(const ScopePtr& scope, Location loca TypeId nextDiscriminantTy = arena->addType(TableType{}); NotNull table{getMutable(nextDiscriminantTy)}; - table->props[*key->propName] = {discriminantTy}; + table->props[*key->propName] = Property::readonly(discriminantTy); table->scope = scope.get(); table->state = TableState::Sealed; @@ -1894,7 +1910,7 @@ Inference ConstraintGenerator::checkIndexName(const ScopePtr& scope, const Refin scope->rvalueRefinements[key->def] = result; } - addConstraint(scope, indexee->location, HasPropConstraint{result, obj, std::move(index)}); + addConstraint(scope, indexee->location, HasPropConstraint{result, obj, std::move(index), ValueContext::RValue}); if (key) return Inference{result, refinementArena.proposition(key, builtinTypes->truthyType)}; @@ -1945,27 +1961,30 @@ Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprFunction* fun { Checkpoint startCheckpoint = checkpoint(this); FunctionSignature sig = checkFunctionSignature(scope, func, expectedType); + + interiorTypes.push_back(std::vector{}); checkFunctionBody(sig.bodyScope, func); Checkpoint endCheckpoint = checkpoint(this); + TypeId generalizedTy = arena->addType(BlockedType{}); + NotNull gc = addConstraint(sig.signatureScope, func->location, GeneralizationConstraint{generalizedTy, sig.signature, std::move(interiorTypes.back())}); + interiorTypes.pop_back(); + + Constraint* previous = nullptr; + forEachConstraint(startCheckpoint, endCheckpoint, this, [gc, &previous](const ConstraintPtr& constraint) { + gc->dependencies.emplace_back(constraint.get()); + + if (auto psc = get(*constraint); psc && psc->returns) + { + if (previous) + constraint->dependencies.push_back(NotNull{previous}); + + previous = constraint.get(); + } + }); + if (generalize && hasFreeType(sig.signature)) { - TypeId generalizedTy = arena->addType(BlockedType{}); - NotNull gc = addConstraint(sig.signatureScope, func->location, GeneralizationConstraint{generalizedTy, sig.signature}); - - Constraint* previous = nullptr; - forEachConstraint(startCheckpoint, endCheckpoint, this, [gc, &previous](const ConstraintPtr& constraint) { - gc->dependencies.emplace_back(constraint.get()); - - if (auto psc = get(*constraint); psc && psc->returns) - { - if (previous) - constraint->dependencies.push_back(NotNull{previous}); - - previous = constraint.get(); - } - }); - return Inference{generalizedTy}; } else @@ -2529,7 +2548,8 @@ TypeId ConstraintGenerator::updateProperty(const ScopePtr& scope, AstExpr* expr, { TypeId segmentTy = arena->addType(BlockedType{}); module->astTypes[exprs[i]] = segmentTy; - addConstraint(scope, expr->location, HasPropConstraint{segmentTy, prevSegmentTy, segments[i]}); + ValueContext ctx = i == segments.size() - 1 ? ValueContext::LValue : ValueContext::RValue; + addConstraint(scope, expr->location, HasPropConstraint{segmentTy, prevSegmentTy, segments[i], ctx}); prevSegmentTy = segmentTy; } @@ -2563,6 +2583,8 @@ Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprTable* expr, ttv->state = TableState::Unsealed; ttv->scope = scope.get(); + interiorTypes.back().push_back(ty); + auto createIndexer = [this, scope, ttv](const Location& location, TypeId currentIndexType, TypeId currentResultType) { if (!ttv->indexer) { @@ -2613,7 +2635,8 @@ Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprTable* expr, { expectedValueType = arena->addType(BlockedType{}); addConstraint(scope, item.value->location, - HasPropConstraint{*expectedValueType, *expectedType, stringKey->value.data, /*suppressSimplification*/ true}); + HasPropConstraint{ + *expectedValueType, *expectedType, stringKey->value.data, ValueContext::RValue, /*suppressSimplification*/ true}); } } } @@ -2876,15 +2899,10 @@ ConstraintGenerator::FunctionSignature ConstraintGenerator::checkFunctionSignatu void ConstraintGenerator::checkFunctionBody(const ScopePtr& scope, AstExprFunction* fn) { - visitBlockWithoutChildScope(scope, fn->body); - // If it is possible for execution to reach the end of the function, the return type must be compatible with () - - if (nullptr != getFallthrough(fn->body)) - { - TypePackId empty = arena->addTypePack({}); // TODO we could have CG retain one of these forever - addConstraint(scope, fn->location, PackSubtypeConstraint{scope->returnType, empty}); - } + ControlFlow cf = visitBlockWithoutChildScope(scope, fn->body); + if (cf == ControlFlow::None) + addConstraint(scope, fn->location, PackSubtypeConstraint{builtinTypes->emptyTypePack, scope->returnType}); } TypeId ConstraintGenerator::resolveType(const ScopePtr& scope, AstType* ty, bool inTypeArguments, bool replaceErrorWithFresh) @@ -2977,20 +2995,30 @@ TypeId ConstraintGenerator::resolveType(const ScopePtr& scope, AstType* ty, bool for (const AstTableProp& prop : tab->props) { - if (prop.access == AstTableAccess::Read) - reportError(prop.accessLocation.value_or(Location{}), GenericError{"read keyword is illegal here"}); - else if (prop.access == AstTableAccess::Write) - reportError(prop.accessLocation.value_or(Location{}), GenericError{"write keyword is illegal here"}); - else if (prop.access == AstTableAccess::ReadWrite) + // TODO: Recursion limit. + TypeId propTy = resolveType(scope, prop.type, inTypeArguments); + + Property& p = props[prop.name.value]; + p.typeLocation = prop.location; + + switch (prop.access) { - std::string name = prop.name.value; - // TODO: Recursion limit. - TypeId propTy = resolveType(scope, prop.type, inTypeArguments); - props[name] = {propTy}; - props[name].typeLocation = prop.location; - } - else + case AstTableAccess::ReadWrite: + p.readTy = propTy; + p.writeTy = propTy; + break; + case AstTableAccess::Read: + p.readTy = propTy; + break; + case AstTableAccess::Write: + reportError(*prop.accessLocation, GenericError{"write keyword is illegal here"}); + p.readTy = propTy; + p.writeTy = propTy; + break; + default: ice->ice("Unexpected property access " + std::to_string(int(prop.access))); + break; + } } if (AstTableIndexer* astIndexer = tab->indexer) diff --git a/Analysis/src/ConstraintSolver.cpp b/Analysis/src/ConstraintSolver.cpp index 109fb2fb..996d638b 100644 --- a/Analysis/src/ConstraintSolver.cpp +++ b/Analysis/src/ConstraintSolver.cpp @@ -6,6 +6,7 @@ #include "Luau/ConstraintSolver.h" #include "Luau/DcrLogger.h" #include "Luau/Instantiation.h" +#include "Luau/Instantiation2.h" #include "Luau/Location.h" #include "Luau/ModuleResolver.h" #include "Luau/OverloadResolution.h" @@ -430,8 +431,6 @@ void ConstraintSolver::run() progress |= runSolverPass(true); } while (progress); - finalizeModule(); - if (FFlag::DebugLuauLogSolver) { dumpBindings(rootScope, opts); @@ -477,48 +476,6 @@ struct FreeTypeSearcher : TypeOnceVisitor } // namespace -void ConstraintSolver::finalizeModule() -{ - Anyification a{arena, rootScope, builtinTypes, &iceReporter, builtinTypes->anyType, builtinTypes->anyTypePack}; - std::optional returnType = a.substitute(rootScope->returnType); - if (!returnType) - { - reportError(CodeTooComplex{}, Location{}); - rootScope->returnType = builtinTypes->errorTypePack; - } - else - { - rootScope->returnType = anyifyModuleReturnTypePackGenerics(*returnType); - } - - Unifier2 u2{NotNull{arena}, builtinTypes, rootScope, NotNull{&iceReporter}}; - - VecDeque queue; - for (auto& [name, binding] : rootScope->bindings) - queue.push_back({binding.typeId, binding.location}); - - DenseHashSet seen{nullptr}; - - while (!queue.empty()) - { - TypeAndLocation binding = queue.front(); - queue.pop_front(); - - TypeId ty = follow(binding.typeId); - - if (seen.find(ty)) - continue; - seen.insert(ty); - - FreeTypeSearcher fts{&queue, binding.location}; - fts.traverse(ty); - - auto result = u2.generalize(ty); - if (!result) - reportError(CodeTooComplex{}, binding.location); - } -} - bool ConstraintSolver::tryDispatch(NotNull constraint, bool force) { if (!force && isBlocked(constraint)) @@ -562,6 +519,8 @@ bool ConstraintSolver::tryDispatch(NotNull constraint, bool fo success = tryDispatch(*rc, constraint, force); else if (auto rpc = get(*constraint)) success = tryDispatch(*rpc, constraint, force); + else if (auto eqc = get(*constraint)) + success = tryDispatch(*eqc, constraint, force); else LUAU_ASSERT(false); @@ -578,7 +537,9 @@ bool ConstraintSolver::tryDispatch(const SubtypeConstraint& c, NotNull constraint, bool force) @@ -588,7 +549,9 @@ bool ConstraintSolver::tryDispatch(const PackSubtypeConstraint& c, NotNull constraint, bool force) @@ -615,13 +578,13 @@ bool ConstraintSolver::tryDispatch(const GeneralizationConstraint& c, NotNull(generalizedType)) asMutable(generalizedType)->ty.emplace(generalized->result); else - unify(constraint->scope, constraint->location, generalizedType, generalized->result); + unify(constraint, generalizedType, generalized->result); for (auto [free, gen] : generalized->insertedGenerics.pairings) - unify(constraint->scope, constraint->location, free, gen); + unify(constraint, free, gen); for (auto [free, gen] : generalized->insertedGenericPacks.pairings) - unify(constraint->scope, constraint->location, free, gen); + unify(constraint, free, gen); } else { @@ -632,6 +595,9 @@ bool ConstraintSolver::tryDispatch(const GeneralizationConstraint& c, NotNulllocation); unblock(c.sourceType, constraint->location); + for (TypeId ty : c.interiorTypes) + u2.generalize(ty); + return true; } @@ -730,7 +696,7 @@ bool ConstraintSolver::tryDispatch(const IterableConstraint& c, NotNullscope, builtinTypes, &iceReporter, errorRecoveryType(), errorRecoveryTypePack()}; std::optional anyified = anyify.substitute(c.variables); LUAU_ASSERT(anyified); - unify(constraint->scope, constraint->location, *anyified, c.variables); + unify(constraint, *anyified, c.variables); return true; } @@ -1112,6 +1078,26 @@ bool ConstraintSolver::tryDispatch(const FunctionCallConstraint& c, NotNull subst = instantiation.substitute(result); + + if (!subst) + { + reportError(CodeTooComplex{}, constraint->location); + result = builtinTypes->errorTypePack; + } + else + { + result = *subst; + } + + if (c.result != result) + asMutable(c.result)->ty.emplace(result); + } + for (const auto& [expanded, additions] : u2.expandedFreeTypes) { for (TypeId addition : additions) @@ -1132,7 +1118,7 @@ bool ConstraintSolver::tryDispatch(const FunctionCallConstraint& c, NotNull constraint) { - const TypeId fn = follow(c.fn); + TypeId fn = follow(c.fn); const TypePackId argsPack = follow(c.argsPack); if (isBlocked(fn)) @@ -1156,6 +1142,35 @@ bool ConstraintSolver::tryDispatch(const FunctionCheckConstraint& c, NotNull replacements{nullptr}; + DenseHashMap replacementPacks{nullptr}; + + for (auto generic : ftv->generics) + replacements[generic] = builtinTypes->unknownType; + + for (auto genericPack : ftv->genericPacks) + replacementPacks[genericPack] = builtinTypes->unknownTypePack; + + // If the type of the function has generics, we don't actually want to push any of the generics themselves + // into the argument types as expected types because this creates an unnecessary loop. Instead, we want to + // replace these types with `unknown` (and `...unknown`) to keep any structure but not create the cycle. + if (!replacements.empty() || !replacementPacks.empty()) + { + Replacer replacer{arena, std::move(replacements), std::move(replacementPacks)}; + + std::optional res = replacer.substitute(fn); + if (res) + { + fn = *res; + ftv = get(*res); + LUAU_ASSERT(ftv); + + // we've potentially copied type families here, so we need to reproduce their reduce constraint. + reproduceConstraints(constraint->scope, constraint->location, replacer); + } + } + + const std::vector expectedArgs = flatten(ftv->argTypes).first; const std::vector argPackHead = flatten(argsPack).first; @@ -1237,7 +1252,7 @@ bool ConstraintSolver::tryDispatch(const HasPropConstraint& c, NotNull(subjectType) || get(subjectType)) return block(subjectType, constraint); - auto [blocked, result] = lookupTableProp(subjectType, c.prop, c.suppressSimplification); + auto [blocked, result] = lookupTableProp(subjectType, c.prop, c.context, c.suppressSimplification); if (!blocked.empty()) { for (TypeId blocked : blocked) @@ -1304,7 +1319,7 @@ static void updateTheTableType( for (size_t i = 0; i < path.size() - 1; ++i) { t = follow(t); - auto propTy = findTablePropertyRespectingMeta(builtinTypes, dummy, t, path[i], Location{}); + auto propTy = findTablePropertyRespectingMeta(builtinTypes, dummy, t, path[i], ValueContext::LValue, Location{}); dummy.clear(); if (!propTy) @@ -1329,17 +1344,26 @@ static void updateTheTableType( bool ConstraintSolver::tryDispatch(const SetPropConstraint& c, NotNull constraint, bool force) { TypeId subjectType = follow(c.subjectType); + const TypeId propType = follow(c.propType); if (isBlocked(subjectType)) return block(subjectType, constraint); std::optional existingPropType = subjectType; - for (const std::string& segment : c.path) + + LUAU_ASSERT(!c.path.empty()); + if (c.path.empty()) + return false; + + for (size_t i = 0; i < c.path.size(); ++i) { + const std::string& segment = c.path[i]; if (!existingPropType) break; - auto [blocked, result] = lookupTableProp(*existingPropType, segment); + ValueContext ctx = i == c.path.size() - 1 ? ValueContext::LValue : ValueContext::RValue; + + auto [blocked, result] = lookupTableProp(*existingPropType, segment, ctx); if (!blocked.empty()) { for (TypeId blocked : blocked) @@ -1356,8 +1380,8 @@ bool ConstraintSolver::tryDispatch(const SetPropConstraint& c, NotNullscope, constraint->location, c.propType, *existingPropType); + unify(constraint->scope, constraint->location, propType, *existingPropType); + unify(constraint->scope, constraint->location, *existingPropType, propType); bind(c.resultType, c.subjectType); unblock(c.resultType, constraint->location); return true; @@ -1382,7 +1406,7 @@ bool ConstraintSolver::tryDispatch(const SetPropConstraint& c, NotNullpersistent); - ttv->props[c.path[0]] = Property{c.propType}; + ttv->props[c.path[0]] = Property{propType}; bind(c.resultType, subjectType); unblock(c.resultType, constraint->location); return true; @@ -1391,7 +1415,7 @@ bool ConstraintSolver::tryDispatch(const SetPropConstraint& c, NotNullpersistent); - updateTheTableType(builtinTypes, NotNull{arena}, subjectType, c.path, c.propType); + updateTheTableType(builtinTypes, NotNull{arena}, subjectType, c.path, propType); } } @@ -1425,7 +1449,7 @@ bool ConstraintSolver::tryDispatch(const SetIndexerConstraint& c, NotNullindexer) { // TODO This probably has to be invariant. - unify(constraint->scope, constraint->location, c.indexType, tt->indexer->indexType); + unify(constraint, c.indexType, tt->indexer->indexType); asMutable(c.propType)->ty.emplace(tt->indexer->indexResultType); asMutable(c.resultType)->ty.emplace(subjectType); unblock(c.propType, constraint->location); @@ -1435,7 +1459,7 @@ bool ConstraintSolver::tryDispatch(const SetIndexerConstraint& c, NotNullstate == TableState::Free || tt->state == TableState::Unsealed) { TypeId promotedIndexTy = freshType(arena, builtinTypes, tt->scope); - unify(constraint->scope, constraint->location, c.indexType, promotedIndexTy); + unify(constraint, c.indexType, promotedIndexTy); auto mtt = getMutable(subjectType); mtt->indexer = TableIndexer{promotedIndexTy, c.propType}; @@ -1486,6 +1510,7 @@ bool ConstraintSolver::tryDispatch(const UnpackConstraint& c, NotNullty.emplace(sourcePack); unblock(resultPack, constraint->location); return true; @@ -1525,7 +1550,7 @@ bool ConstraintSolver::tryDispatch(const UnpackConstraint& c, NotNullscope, constraint->location, resultTy, srcTy); + unify(constraint, resultTy, srcTy); } unblock(resultTy, constraint->location); @@ -1553,7 +1578,7 @@ bool ConstraintSolver::tryDispatch(const UnpackConstraint& c, NotNullscope, constraint->location, resultTy, srcTy); + unify(constraint, resultTy, srcTy); ++resultIter; ++i; @@ -1616,7 +1641,9 @@ bool ConstraintSolver::tryDispatch(const SetOpConstraint& c, NotNull constraint, bool force) { TypeId ty = follow(c.ty); - FamilyGraphReductionResult result = reduceFamilies(ty, constraint->location, TypeFamilyContext{NotNull{this}, constraint->scope}, force); + FamilyGraphReductionResult result = + reduceFamilies(ty, constraint->location, TypeFamilyContext{NotNull{this}, constraint->scope, constraint}, force); + for (TypeId r : result.reducedTypes) unblock(r, constraint->location); @@ -1639,7 +1666,8 @@ bool ConstraintSolver::tryDispatch(const ReduceConstraint& c, NotNull constraint, bool force) { TypePackId tp = follow(c.tp); - FamilyGraphReductionResult result = reduceFamilies(tp, constraint->location, TypeFamilyContext{NotNull{this}, constraint->scope}, force); + FamilyGraphReductionResult result = + reduceFamilies(tp, constraint->location, TypeFamilyContext{NotNull{this}, constraint->scope, constraint}, force); for (TypeId r : result.reducedTypes) unblock(r, constraint->location); @@ -1659,6 +1687,13 @@ bool ConstraintSolver::tryDispatch(const ReducePackConstraint& c, NotNull constraint, bool force) +{ + unify(constraint->scope, constraint->location, c.resultType, c.assignmentType); + unify(constraint->scope, constraint->location, c.assignmentType, c.resultType); + return true; +} + bool ConstraintSolver::tryDispatchIterableTable(TypeId iteratorTy, const IterableConstraint& c, NotNull constraint, bool force) { auto block_ = [&](auto&& t) { @@ -1721,7 +1756,7 @@ bool ConstraintSolver::tryDispatchIterableTable(TypeId iteratorTy, const Iterabl if (iteratorTable->indexer) { TypePackId expectedVariablePack = arena->addTypePack({iteratorTable->indexer->indexType, iteratorTable->indexer->indexResultType}); - unify(constraint->scope, constraint->location, c.variables, expectedVariablePack); + unify(constraint, c.variables, expectedVariablePack); auto [variableTys, variablesTail] = flatten(c.variables); @@ -1755,7 +1790,7 @@ bool ConstraintSolver::tryDispatchIterableTable(TypeId iteratorTy, const Iterabl if (auto iterFtv = get(*instantiatedIterFn)) { TypePackId expectedIterArgs = arena->addTypePack({iteratorTy}); - unify(constraint->scope, constraint->location, iterFtv->argTypes, expectedIterArgs); + unify(constraint, iterFtv->argTypes, expectedIterArgs); TypePack iterRets = extendTypePack(*arena, builtinTypes, iterFtv->retTypes, 2); @@ -1779,7 +1814,7 @@ bool ConstraintSolver::tryDispatchIterableTable(TypeId iteratorTy, const Iterabl const TypePackId nextRetPack = arena->addTypePack(TypePack{{firstIndex}, valueTailTy}); const TypeId expectedNextTy = arena->addType(FunctionType{nextArgPack, nextRetPack}); - unify(constraint->scope, constraint->location, *instantiatedNextFn, expectedNextTy); + unify(constraint, *instantiatedNextFn, expectedNextTy); pushConstraint(constraint->scope, constraint->location, UnpackConstraint{c.variables, nextRetPack}); } @@ -1849,13 +1884,11 @@ bool ConstraintSolver::tryDispatchIterableFunction( const TypePackId nextRetPack = arena->addTypePack(TypePack{{retIndex}, valueTailTy}); const TypeId expectedNextTy = arena->addType(FunctionType{TypeLevel{}, constraint->scope, nextArgPack, nextRetPack}); - std::optional error = unify(constraint->scope, constraint->location, nextTy, expectedNextTy); + bool ok = unify(constraint, nextTy, expectedNextTy); // if there are no errors from unifying the two, we can pass forward the expected type as our selected resolution. - if (!error) + if (ok) (*c.astForInNextTypes)[c.nextAstFragment] = expectedNextTy; - else - reportError(*error); auto it = begin(nextRetPack); std::vector modifiedNextRetHead; @@ -1882,14 +1915,14 @@ bool ConstraintSolver::tryDispatchIterableFunction( } std::pair, std::optional> ConstraintSolver::lookupTableProp( - TypeId subjectType, const std::string& propName, bool suppressSimplification) + TypeId subjectType, const std::string& propName, ValueContext context, bool suppressSimplification) { DenseHashSet seen{nullptr}; - return lookupTableProp(subjectType, propName, suppressSimplification, seen); + return lookupTableProp(subjectType, propName, context, suppressSimplification, seen); } std::pair, std::optional> ConstraintSolver::lookupTableProp( - TypeId subjectType, const std::string& propName, bool suppressSimplification, DenseHashSet& seen) + TypeId subjectType, const std::string& propName, ValueContext context, bool suppressSimplification, DenseHashSet& seen) { if (seen.contains(subjectType)) return {}; @@ -1906,19 +1939,58 @@ std::pair, std::optional> ConstraintSolver::lookupTa else if (auto ttv = getMutable(subjectType)) { if (auto prop = ttv->props.find(propName); prop != ttv->props.end()) - return {{}, FFlag::DebugLuauReadWriteProperties ? prop->second.readType() : prop->second.type()}; - else if (ttv->indexer && maybeString(ttv->indexer->indexType)) + { + switch (context) + { + case ValueContext::RValue: + if (auto rt = prop->second.readTy) + return {{}, rt}; + break; + case ValueContext::LValue: + if (auto wt = prop->second.writeTy) + return {{}, wt}; + break; + } + } + + if (ttv->indexer && maybeString(ttv->indexer->indexType)) return {{}, ttv->indexer->indexResultType}; - else if (ttv->state == TableState::Free) + + if (ttv->state == TableState::Free) { TypeId result = freshType(arena, builtinTypes, ttv->scope); - ttv->props[propName] = Property{result}; + switch (context) + { + case ValueContext::RValue: + ttv->props[propName].readTy = result; + break; + case ValueContext::LValue: + if (auto it = ttv->props.find(propName); it != ttv->props.end() && it->second.isReadOnly()) + { + // We do infer read-only properties, but we do not infer + // separate read and write types. + // + // If we encounter a case where a free table has a read-only + // property that we subsequently sense a write to, we make + // the judgement that the property is read-write and that + // both the read and write types are the same. + + Property& prop = it->second; + + prop.writeTy = prop.readTy; + return {{}, *prop.readTy}; + } + else + ttv->props[propName] = Property::rw(result); + + break; + } return {{}, result}; } } else if (auto mt = get(subjectType)) { - auto [blocked, result] = lookupTableProp(mt->table, propName, suppressSimplification, seen); + auto [blocked, result] = lookupTableProp(mt->table, propName, context, suppressSimplification, seen); if (!blocked.empty() || result) return {blocked, result}; @@ -1949,13 +2021,13 @@ std::pair, std::optional> ConstraintSolver::lookupTa } } else - return lookupTableProp(indexType, propName, suppressSimplification, seen); + return lookupTableProp(indexType, propName, context, suppressSimplification, seen); } } else if (auto ct = get(subjectType)) { if (auto p = lookupClassProp(ct, propName)) - return {{}, p->type()}; + return {{}, context == ValueContext::RValue ? p->readTy : p->writeTy}; if (ct->indexer) { return {{}, ct->indexer->indexResultType}; @@ -1970,14 +2042,14 @@ std::pair, std::optional> ConstraintSolver::lookupTa if (indexProp == metatable->props.end()) return {{}, std::nullopt}; - return lookupTableProp(indexProp->second.type(), propName, suppressSimplification, seen); + return lookupTableProp(indexProp->second.type(), propName, context, suppressSimplification, seen); } else if (auto ft = get(subjectType)) { const TypeId upperBound = follow(ft->upperBound); if (get(upperBound) || get(upperBound)) - return lookupTableProp(upperBound, propName, suppressSimplification, seen); + return lookupTableProp(upperBound, propName, context, suppressSimplification, seen); // TODO: The upper bound could be an intersection that contains suitable tables or classes. @@ -1987,7 +2059,16 @@ std::pair, std::optional> ConstraintSolver::lookupTa TableType* tt = getMutable(newUpperBound); LUAU_ASSERT(tt); TypeId propType = freshType(arena, builtinTypes, scope); - tt->props[propName] = Property{propType}; + + switch (context) + { + case ValueContext::RValue: + tt->props[propName] = Property::readonly(propType); + break; + case ValueContext::LValue: + tt->props[propName] = Property::rw(propType); + break; + } unify(scope, Location{}, subjectType, newUpperBound); @@ -2000,7 +2081,7 @@ std::pair, std::optional> ConstraintSolver::lookupTa for (TypeId ty : utv) { - auto [innerBlocked, innerResult] = lookupTableProp(ty, propName, suppressSimplification, seen); + auto [innerBlocked, innerResult] = lookupTableProp(ty, propName, context, suppressSimplification, seen); blocked.insert(blocked.end(), innerBlocked.begin(), innerBlocked.end()); if (innerResult) options.insert(*innerResult); @@ -2029,7 +2110,7 @@ std::pair, std::optional> ConstraintSolver::lookupTa for (TypeId ty : itv) { - auto [innerBlocked, innerResult] = lookupTableProp(ty, propName, suppressSimplification, seen); + auto [innerBlocked, innerResult] = lookupTableProp(ty, propName, context, suppressSimplification, seen); blocked.insert(blocked.end(), innerBlocked.begin(), innerBlocked.end()); if (innerResult) options.insert(*innerResult); @@ -2055,34 +2136,39 @@ std::pair, std::optional> ConstraintSolver::lookupTa return {{}, std::nullopt}; } -template -bool ConstraintSolver::tryUnify(NotNull constraint, TID subTy, TID superTy) +template +bool ConstraintSolver::unify(NotNull scope, Location location, TID subType, TID superType) { - Unifier2 u2{NotNull{arena}, builtinTypes, constraint->scope, NotNull{&iceReporter}}; + Unifier2 u2{NotNull{arena}, builtinTypes, scope, NotNull{&iceReporter}}; - bool success = u2.unify(subTy, superTy); + const bool ok = u2.unify(subType, superType); - if (success) + if (ok) { for (const auto& [expanded, additions] : u2.expandedFreeTypes) { for (TypeId addition : additions) - upperBoundContributors[expanded].push_back(std::make_pair(constraint->location, addition)); + upperBoundContributors[expanded].push_back(std::make_pair(location, addition)); } } else { - // Unification only fails when doing so would fail the occurs check. - // ie create a self-bound type or a cyclic type pack - reportError(OccursCheckFailed{}, constraint->location); + reportError(OccursCheckFailed{}, location); + return false; } - unblock(subTy, constraint->location); - unblock(superTy, constraint->location); + unblock(subType, location); + unblock(superType, location); return true; } +template +bool ConstraintSolver::unify(NotNull constraint, TID subTy, TID superTy) +{ + return unify(constraint->scope, constraint->location, subTy, superTy); +} + void ConstraintSolver::bindBlockedType(TypeId blockedTy, TypeId resultTy, TypeId rootTy, Location location) { resultTy = follow(resultTy); @@ -2297,6 +2383,21 @@ void ConstraintSolver::unblock(const std::vector& packs, Location lo unblock(t, location); } +void ConstraintSolver::reproduceConstraints(NotNull scope, const Location& location, const Substitution& subst) +{ + for (auto [_, newTy] : subst.newTypes) + { + if (get(newTy)) + pushConstraint(scope, location, ReduceConstraint{newTy}); + } + + for (auto [_, newPack] : subst.newPacks) + { + if (get(newPack)) + pushConstraint(scope, location, ReducePackConstraint{newPack}); + } +} + bool ConstraintSolver::isBlocked(TypeId ty) { ty = follow(ty); @@ -2318,39 +2419,6 @@ bool ConstraintSolver::isBlocked(NotNull constraint) return blockedIt != blockedConstraints.end() && blockedIt->second > 0; } -std::optional ConstraintSolver::unify(NotNull scope, Location location, TypeId subType, TypeId superType) -{ - Unifier2 u2{NotNull{arena}, builtinTypes, scope, NotNull{&iceReporter}}; - - const bool ok = u2.unify(subType, superType); - - if (!ok) - return {{location, UnificationTooComplex{}}}; - - unblock(subType, Location{}); - unblock(superType, Location{}); - - return {}; -} - -ErrorVec ConstraintSolver::unify(NotNull scope, Location location, TypePackId subPack, TypePackId superPack) -{ - Unifier2 u{arena, builtinTypes, scope, NotNull{&iceReporter}}; - - u.unify(subPack, superPack); - - for (const auto& [expanded, additions] : u.expandedFreeTypes) - { - for (TypeId addition : additions) - upperBoundContributors[expanded].push_back(std::make_pair(location, addition)); - } - - unblock(subPack, Location{}); - unblock(superPack, Location{}); - - return {}; -} - NotNull ConstraintSolver::pushConstraint(NotNull scope, const Location& location, ConstraintV cv) { std::unique_ptr c = std::make_unique(scope, location, std::move(cv)); diff --git a/Analysis/src/Error.cpp b/Analysis/src/Error.cpp index ba4e3a35..d1516b09 100644 --- a/Analysis/src/Error.cpp +++ b/Analysis/src/Error.cpp @@ -541,11 +541,36 @@ struct ErrorConverter "' is used in a way that will run time error"; } + std::string operator()(const PropertyAccessViolation& e) const + { + const std::string stringKey = isIdentifier(e.key) ? e.key : "\"" + e.key + "\""; + switch (e.context) + { + case PropertyAccessViolation::CannotRead: + return "Property " + stringKey + " of table '" + toString(e.table) + "' is write-only"; + case PropertyAccessViolation::CannotWrite: + return "Property " + stringKey + " of table '" + toString(e.table) + "' is read-only"; + } + + LUAU_UNREACHABLE(); + return ""; + } + std::string operator()(const CheckedFunctionIncorrectArgs& e) const { return "Checked Function " + e.functionName + " expects " + std::to_string(e.expected) + " arguments, but received " + std::to_string(e.actual); } + + std::string operator()(const UnexpectedTypeInSubtyping& e) const + { + return "Encountered an unexpected type in subtyping: " + toString(e.ty); + } + + std::string operator()(const UnexpectedTypePackInSubtyping& e) const + { + return "Encountered an unexpected type pack in subtyping: " + toString(e.tp); + } }; struct InvalidNameChecker @@ -638,6 +663,11 @@ bool UnknownProperty::operator==(const UnknownProperty& rhs) const return *table == *rhs.table && key == rhs.key; } +bool PropertyAccessViolation::operator==(const PropertyAccessViolation& rhs) const +{ + return *table == *rhs.table && key == rhs.key && context == rhs.context; +} + bool NotATable::operator==(const NotATable& rhs) const { return ty == rhs.ty; @@ -884,6 +914,16 @@ bool CheckedFunctionIncorrectArgs::operator==(const CheckedFunctionIncorrectArgs return functionName == rhs.functionName && expected == rhs.expected && actual == rhs.actual; } +bool UnexpectedTypeInSubtyping::operator==(const UnexpectedTypeInSubtyping& rhs) const +{ + return ty == rhs.ty; +} + +bool UnexpectedTypePackInSubtyping::operator==(const UnexpectedTypePackInSubtyping& rhs) const +{ + return tp == rhs.tp; +} + std::string toString(const TypeError& error) { return toString(error, TypeErrorToStringOptions{}); @@ -1059,9 +1099,15 @@ void copyError(T& e, TypeArena& destArena, CloneState& cloneState) { e.argumentType = clone(e.argumentType); } + else if constexpr (std::is_same_v) + e.table = clone(e.table); else if constexpr (std::is_same_v) { } + else if constexpr (std::is_same_v) + e.ty = clone(e.ty); + else if constexpr (std::is_same_v) + e.tp = clone(e.tp); else static_assert(always_false_v, "Non-exhaustive type switch"); } diff --git a/Analysis/src/Frontend.cpp b/Analysis/src/Frontend.cpp index b0dadd0b..82b78149 100644 --- a/Analysis/src/Frontend.cpp +++ b/Analysis/src/Frontend.cpp @@ -32,9 +32,8 @@ LUAU_FASTINT(LuauTypeInferRecursionLimit) LUAU_FASTINT(LuauTarjanChildLimit) LUAU_FASTFLAG(LuauInferInNoCheckMode) LUAU_FASTFLAGVARIABLE(LuauKnowsTheDataModel3, false) -LUAU_FASTFLAGVARIABLE(DebugLuauDeferredConstraintResolution, false) +LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution) LUAU_FASTFLAGVARIABLE(DebugLuauLogSolverToJson, false) -LUAU_FASTFLAGVARIABLE(DebugLuauReadWriteProperties, false) LUAU_FASTFLAGVARIABLE(DebugLuauLogSolverToJsonFile, false) namespace Luau @@ -1219,6 +1218,15 @@ ModulePtr check(const SourceModule& sourceModule, Mode mode, const std::vectorcancelled = true; } + if (recordJsonLog) + { + std::string output = logger->compileOutput(); + if (FFlag::DebugLuauLogSolverToJsonFile && writeJsonLog) + writeJsonLog(sourceModule.name, std::move(output)); + else + printf("%s\n", output.c_str()); + } + for (TypeError& e : cs.errors) result->errors.emplace_back(std::move(e)); @@ -1263,15 +1271,6 @@ ModulePtr check(const SourceModule& sourceModule, Mode mode, const std::vectorinternalTypes); freeze(result->interfaceTypes); - if (recordJsonLog) - { - std::string output = logger->compileOutput(); - if (FFlag::DebugLuauLogSolverToJsonFile && writeJsonLog) - writeJsonLog(sourceModule.name, std::move(output)); - else - printf("%s\n", output.c_str()); - } - return result; } diff --git a/Analysis/src/Instantiation.cpp b/Analysis/src/Instantiation.cpp index 235786a8..525319c6 100644 --- a/Analysis/src/Instantiation.cpp +++ b/Analysis/src/Instantiation.cpp @@ -2,6 +2,7 @@ #include "Luau/Instantiation.h" #include "Luau/Common.h" +#include "Luau/Instantiation2.h" // including for `Replacer` which was stolen since it will be kept in the new solver #include "Luau/ToString.h" #include "Luau/TxnLog.h" #include "Luau/TypeArena.h" @@ -143,39 +144,6 @@ TypePackId ReplaceGenerics::clean(TypePackId tp) return addTypePack(TypePackVar(FreeTypePack{scope, level})); } -struct Replacer : Substitution -{ - DenseHashMap replacements; - DenseHashMap replacementPacks; - - Replacer(NotNull arena, DenseHashMap replacements, DenseHashMap replacementPacks) - : Substitution(TxnLog::empty(), arena) - , replacements(std::move(replacements)) - , replacementPacks(std::move(replacementPacks)) - { - } - - bool isDirty(TypeId ty) override - { - return replacements.find(ty) != nullptr; - } - - bool isDirty(TypePackId tp) override - { - return replacementPacks.find(tp) != nullptr; - } - - TypeId clean(TypeId ty) override - { - return replacements[ty]; - } - - TypePackId clean(TypePackId tp) override - { - return replacementPacks[tp]; - } -}; - std::optional instantiate( NotNull builtinTypes, NotNull arena, NotNull limits, NotNull scope, TypeId ty) { diff --git a/Analysis/src/Instantiation2.cpp b/Analysis/src/Instantiation2.cpp new file mode 100644 index 00000000..31f27f8e --- /dev/null +++ b/Analysis/src/Instantiation2.cpp @@ -0,0 +1,38 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#include "Luau/Instantiation2.h" + +namespace Luau +{ + +bool Instantiation2::isDirty(TypeId ty) +{ + return get(ty) && genericSubstitutions.contains(ty); +} + +bool Instantiation2::isDirty(TypePackId tp) +{ + return get(tp) && genericPackSubstitutions.contains(tp); +} + +TypeId Instantiation2::clean(TypeId ty) +{ + TypeId substTy = genericSubstitutions[ty]; + const FreeType* ft = get(substTy); + + // violation of the substitution invariant if this is not a free type. + LUAU_ASSERT(ft); + + // if we didn't learn anything about the lower bound, we pick the upper bound instead. + if (get(ft->lowerBound)) + return ft->upperBound; + + // we default to the lower bound which represents the most specific type for the free type. + return ft->lowerBound; +} + +TypePackId Instantiation2::clean(TypePackId tp) +{ + return genericPackSubstitutions[tp]; +} + +} // namespace Luau diff --git a/Analysis/src/IostreamHelpers.cpp b/Analysis/src/IostreamHelpers.cpp index 72090848..54afd5d6 100644 --- a/Analysis/src/IostreamHelpers.cpp +++ b/Analysis/src/IostreamHelpers.cpp @@ -207,9 +207,15 @@ static void errorToString(std::ostream& stream, const T& err) else if constexpr (std::is_same_v) stream << "NonStrictFunctionDefinitionError { functionName = '" + err.functionName + "', argument = '" + err.argument + "', argumentType = '" + toString(err.argumentType) + "' }"; + else if constexpr (std::is_same_v) + stream << "PropertyAccessViolation { table = " << toString(err.table) << ", prop = '" << err.key << "', context = " << err.context << " }"; else if constexpr (std::is_same_v) stream << "CheckedFunction { functionName = '" + err.functionName + ", expected = " + std::to_string(err.expected) + ", actual = " + std::to_string(err.actual) + "}"; + else if constexpr (std::is_same_v) + stream << "UnexpectedTypeInSubtyping { ty = '" + toString(err.ty) + "' }"; + else if constexpr (std::is_same_v) + stream << "UnexpectedTypePackInSubtyping { tp = '" + toString(err.tp) + "' }"; else static_assert(always_false_v, "Non-exhaustive type switch"); } diff --git a/Analysis/src/Linter.cpp b/Analysis/src/Linter.cpp index 79a7ef56..d79361c0 100644 --- a/Analysis/src/Linter.cpp +++ b/Analysis/src/Linter.cpp @@ -14,6 +14,8 @@ LUAU_FASTINTVARIABLE(LuauSuggestionDistance, 4) +LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution) + namespace Luau { @@ -1848,6 +1850,49 @@ private: bool visit(AstTypeTable* node) override { + if (FFlag::DebugLuauDeferredConstraintResolution) + { + struct Rec + { + AstTableAccess access; + Location location; + }; + DenseHashMap names(AstName{}); + + for (const AstTableProp& item : node->props) + { + Rec* rec = names.find(item.name); + if (!rec) + { + names[item.name] = Rec{item.access, item.location}; + continue; + } + + if (int(rec->access) & int(item.access)) + { + if (rec->access == item.access) + emitWarning(*context, LintWarning::Code_TableLiteral, item.location, + "Table type field '%s' is a duplicate; previously defined at line %d", item.name.value, rec->location.begin.line + 1); + else if (rec->access == AstTableAccess::ReadWrite) + emitWarning(*context, LintWarning::Code_TableLiteral, item.location, + "Table type field '%s' is already read-write; previously defined at line %d", item.name.value, + rec->location.begin.line + 1); + else if (rec->access == AstTableAccess::Read) + emitWarning(*context, LintWarning::Code_TableLiteral, rec->location, + "Table type field '%s' already has a read type defined at line %d", item.name.value, rec->location.begin.line + 1); + else if (rec->access == AstTableAccess::Write) + emitWarning(*context, LintWarning::Code_TableLiteral, rec->location, + "Table type field '%s' already has a write type defined at line %d", item.name.value, rec->location.begin.line + 1); + else + LUAU_ASSERT(!"Unreachable"); + } + else + rec->access = AstTableAccess(int(rec->access) | int(item.access)); + } + + return true; + } + DenseHashMap names(AstName{}); for (const AstTableProp& item : node->props) diff --git a/Analysis/src/Normalize.cpp b/Analysis/src/Normalize.cpp index ee80cc99..62aeb145 100644 --- a/Analysis/src/Normalize.cpp +++ b/Analysis/src/Normalize.cpp @@ -9,6 +9,7 @@ #include "Luau/Common.h" #include "Luau/RecursionCounter.h" #include "Luau/Set.h" +#include "Luau/Simplify.h" #include "Luau/Subtyping.h" #include "Luau/Type.h" #include "Luau/TypeFwd.h" @@ -20,7 +21,6 @@ LUAU_FASTFLAGVARIABLE(DebugLuauCheckNormalizeInvariant, false) LUAU_FASTINTVARIABLE(LuauNormalizeIterationLimit, 1200); LUAU_FASTINTVARIABLE(LuauNormalizeCacheLimit, 100000); LUAU_FASTFLAG(LuauTransitiveSubtyping) -LUAU_FASTFLAG(DebugLuauReadWriteProperties) LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution) namespace Luau @@ -472,11 +472,11 @@ bool Normalizer::isInhabited(TypeId ty, Set& seen) { for (const auto& [_, prop] : ttv->props) { - if (FFlag::DebugLuauReadWriteProperties) + if (FFlag::DebugLuauDeferredConstraintResolution) { // A table enclosing a read property whose type is uninhabitable is also itself uninhabitable, // but not its write property. That just means the write property doesn't exist, and so is readonly. - if (auto ty = prop.readType(); ty && !isInhabited(*ty, seen)) + if (auto ty = prop.readTy; ty && !isInhabited(*ty, seen)) return false; } else @@ -2349,12 +2349,61 @@ std::optional Normalizer::intersectionOfTables(TypeId here, TypeId there { const auto& [_name, tprop] = *tfound; // TODO: variance issues here, which can't be fixed until we have read/write property types - prop.setType(intersectionType(hprop.type(), tprop.type())); - hereSubThere &= (prop.type() == hprop.type()); - thereSubHere &= (prop.type() == tprop.type()); + if (FFlag::DebugLuauDeferredConstraintResolution) + { + if (hprop.readTy.has_value()) + { + if (tprop.readTy.has_value()) + { + TypeId ty = simplifyIntersection(builtinTypes, NotNull{arena}, *hprop.readTy, *tprop.readTy).result; + prop.readTy = ty; + hereSubThere &= (ty == hprop.readTy); + thereSubHere &= (ty == tprop.readTy); + } + else + { + prop.readTy = *hprop.readTy; + thereSubHere = false; + } + } + else if (tprop.readTy.has_value()) + { + prop.readTy = *tprop.readTy; + hereSubThere = false; + } + + if (hprop.writeTy.has_value()) + { + if (tprop.writeTy.has_value()) + { + prop.writeTy = simplifyIntersection(builtinTypes, NotNull{arena}, *hprop.writeTy, *tprop.writeTy).result; + hereSubThere &= (prop.writeTy == hprop.writeTy); + thereSubHere &= (prop.writeTy == tprop.writeTy); + } + else + { + prop.writeTy = *hprop.writeTy; + thereSubHere = false; + } + } + else if (tprop.writeTy.has_value()) + { + prop.writeTy = *tprop.writeTy; + hereSubThere = false; + } + } + else + { + prop.setType(intersectionType(hprop.type(), tprop.type())); + hereSubThere &= (prop.type() == hprop.type()); + thereSubHere &= (prop.type() == tprop.type()); + } } + // TODO: string indexers - result.props[name] = prop; + + if (prop.readTy || prop.writeTy) + result.props[name] = prop; } for (const auto& [name, tprop] : tttv->props) @@ -2431,8 +2480,10 @@ void Normalizer::intersectTablesWithTable(TypeIds& heres, TypeId there) { TypeIds tmp; for (TypeId here : heres) + { if (std::optional inter = intersectionOfTables(here, there)) tmp.insert(*inter); + } heres.retain(tmp); heres.insert(tmp.begin(), tmp.end()); } @@ -2441,9 +2492,14 @@ void Normalizer::intersectTables(TypeIds& heres, const TypeIds& theres) { TypeIds tmp; for (TypeId here : heres) + { for (TypeId there : theres) + { if (std::optional inter = intersectionOfTables(here, there)) tmp.insert(*inter); + } + } + heres.retain(tmp); heres.insert(tmp.begin(), tmp.end()); } diff --git a/Analysis/src/Simplify.cpp b/Analysis/src/Simplify.cpp index d04aeb82..dcfc1965 100644 --- a/Analysis/src/Simplify.cpp +++ b/Analysis/src/Simplify.cpp @@ -3,10 +3,10 @@ #include "Luau/Simplify.h" #include "Luau/DenseHash.h" -#include "Luau/Normalize.h" // TypeIds #include "Luau/RecursionCounter.h" -#include "Luau/ToString.h" +#include "Luau/Set.h" #include "Luau/TypeArena.h" +#include "Luau/TypePairHash.h" #include "Luau/TypeUtils.h" #include @@ -17,6 +17,8 @@ LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution) namespace Luau { +using SimplifierSeenSet = Set, TypePairHash>; + struct TypeSimplifier { NotNull builtinTypes; @@ -226,23 +228,27 @@ static bool isTypeVariable(TypeId ty) return get(ty) || get(ty) || get(ty) || get(ty); } -Relation relate(TypeId left, TypeId right); +Relation relate(TypeId left, TypeId right, SimplifierSeenSet& seen); -Relation relateTables(TypeId left, TypeId right) +Relation relateTables(TypeId left, TypeId right, SimplifierSeenSet& seen) { NotNull leftTable{get(left)}; NotNull rightTable{get(right)}; LUAU_ASSERT(1 == rightTable->props.size()); // Disjoint props have nothing in common // t1 with props p1's cannot appear in t2 and t2 with props p2's cannot appear in t1 - bool foundPropFromLeftInRight = std::any_of(begin(leftTable->props), end(leftTable->props), [&](auto prop) { - return rightTable->props.find(prop.first) != end(rightTable->props); - }); - bool foundPropFromRightInLeft = std::any_of(begin(rightTable->props), end(rightTable->props), [&](auto prop) { - return leftTable->props.find(prop.first) != end(leftTable->props); - }); + bool foundPropFromLeftInRight = std::any_of(begin(leftTable->props), end(leftTable->props), + [&](auto prop) + { + return rightTable->props.count(prop.first) > 0; + }); + bool foundPropFromRightInLeft = std::any_of(begin(rightTable->props), end(rightTable->props), + [&](auto prop) + { + return leftTable->props.count(prop.first) > 0; + }); - if (!(foundPropFromLeftInRight || foundPropFromRightInLeft) && leftTable->props.size() >= 1 && rightTable->props.size() >= 1) + if (!foundPropFromLeftInRight && !foundPropFromRightInLeft && leftTable->props.size() >= 1 && rightTable->props.size() >= 1) return Relation::Disjoint; const auto [propName, rightProp] = *begin(rightTable->props); @@ -257,7 +263,10 @@ Relation relateTables(TypeId left, TypeId right) const Property leftProp = it->second; - Relation r = relate(leftProp.type(), rightProp.type()); + if (!leftProp.isShared() || !rightProp.isShared()) + return Relation::Intersects; + + Relation r = relate(leftProp.type(), rightProp.type(), seen); if (r == Relation::Coincident && 1 != leftTable->props.size()) { // eg {tag: "cat", prop: string} & {tag: "cat"} @@ -268,7 +277,7 @@ Relation relateTables(TypeId left, TypeId right) } // A cheap and approximate subtype test -Relation relate(TypeId left, TypeId right) +Relation relate(TypeId left, TypeId right, SimplifierSeenSet& seen) { // TODO nice to have: Relate functions of equal argument and return arity @@ -278,6 +287,14 @@ Relation relate(TypeId left, TypeId right) if (left == right) return Relation::Coincident; + std::pair typePair{left, right}; + if (!seen.insert(typePair)) + { + // TODO: is this right at all? + // The thinking here is that this is a cycle if we get here, and therefore its coincident. + return Relation::Coincident; + } + if (get(left)) { if (get(right)) @@ -291,7 +308,7 @@ Relation relate(TypeId left, TypeId right) } if (get(right)) - return flip(relate(right, left)); + return flip(relate(right, left, seen)); if (get(left)) { @@ -302,7 +319,7 @@ Relation relate(TypeId left, TypeId right) } if (get(right)) - return flip(relate(right, left)); + return flip(relate(right, left, seen)); // Type variables // * FreeType @@ -340,7 +357,7 @@ Relation relate(TypeId left, TypeId right) return Relation::Disjoint; } if (get(right)) - return flip(relate(right, left)); + return flip(relate(right, left, seen)); if (get(left)) { @@ -350,7 +367,7 @@ Relation relate(TypeId left, TypeId right) return Relation::Subset; } if (get(right)) - return flip(relate(right, left)); + return flip(relate(right, left, seen)); if (auto ut = get(left)) return Relation::Intersects; @@ -363,14 +380,14 @@ Relation relate(TypeId left, TypeId right) { std::vector opts; for (TypeId part : ut) - if (relate(left, part) == Relation::Subset) + if (relate(left, part, seen) == Relation::Subset) return Relation::Subset; return Relation::Intersects; } if (auto rnt = get(right)) { - Relation a = relate(left, rnt->ty); + Relation a = relate(left, rnt->ty, seen); switch (a) { case Relation::Coincident: @@ -401,7 +418,7 @@ Relation relate(TypeId left, TypeId right) } } else if (get(left)) - return flip(relate(right, left)); + return flip(relate(right, left, seen)); if (auto lp = get(left)) { @@ -448,7 +465,7 @@ Relation relate(TypeId left, TypeId right) return Relation::Disjoint; if (get(right)) - return flip(relate(right, left)); + return flip(relate(right, left, seen)); if (auto rs = get(right)) { if (ls->variant == rs->variant) @@ -485,7 +502,7 @@ Relation relate(TypeId left, TypeId right) // TODO PROBABLY indexers and metatables. if (1 == rt->props.size()) { - Relation r = relateTables(left, right); + Relation r = relateTables(left, right, seen); /* * A reduction of these intersections is certainly possible, but * it would require minting new table types. Also, I don't think @@ -504,7 +521,7 @@ Relation relate(TypeId left, TypeId right) return r; } else if (1 == lt->props.size()) - return flip(relate(right, left)); + return flip(relate(right, left, seen)); else return Relation::Intersects; } @@ -531,6 +548,13 @@ Relation relate(TypeId left, TypeId right) return Relation::Intersects; } +// A cheap and approximate subtype test +Relation relate(TypeId left, TypeId right) +{ + SimplifierSeenSet seen{{}}; + return relate(left, right, seen); +} + TypeId TypeSimplifier::mkNegation(TypeId ty) { TypeId result = nullptr; @@ -1056,7 +1080,7 @@ std::optional TypeSimplifier::basicIntersect(TypeId left, TypeId right) const auto [propName, leftProp] = *begin(lt->props); auto it = rt->props.find(propName); - if (it != rt->props.end()) + if (it != rt->props.end() && leftProp.isShared() && it->second.isShared()) { Relation r = relate(leftProp.type(), it->second.type()); @@ -1266,9 +1290,12 @@ TypeId TypeSimplifier::simplify(TypeId ty, DenseHashSet& seen) { if (1 == tt->props.size()) { - TypeId propTy = simplify(begin(tt->props)->second.type(), seen); - if (get(propTy)) - return builtinTypes->neverType; + if (std::optional readTy = begin(tt->props)->second.readTy) + { + TypeId propTy = simplify(*readTy, seen); + if (get(propTy)) + return builtinTypes->neverType; + } } } diff --git a/Analysis/src/Substitution.cpp b/Analysis/src/Substitution.cpp index 7da9aee8..0deba9bd 100644 --- a/Analysis/src/Substitution.cpp +++ b/Analysis/src/Substitution.cpp @@ -9,7 +9,7 @@ #include LUAU_FASTINTVARIABLE(LuauTarjanChildLimit, 10000) -LUAU_FASTFLAG(DebugLuauReadWriteProperties) +LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution); LUAU_FASTFLAGVARIABLE(LuauPreallocateTarjanVectors, false); LUAU_FASTINTVARIABLE(LuauTarjanPreallocationSize, 256); @@ -185,10 +185,10 @@ void Tarjan::visitChildren(TypeId ty, int index) LUAU_ASSERT(!ttv->boundTo); for (const auto& [name, prop] : ttv->props) { - if (FFlag::DebugLuauReadWriteProperties) + if (FFlag::DebugLuauDeferredConstraintResolution) { - visitChild(prop.readType()); - visitChild(prop.writeType()); + visitChild(prop.readTy); + visitChild(prop.writeTy); } else visitChild(prop.type()); @@ -700,8 +700,8 @@ void Substitution::replaceChildren(TypeId ty) LUAU_ASSERT(!ttv->boundTo); for (auto& [name, prop] : ttv->props) { - if (FFlag::DebugLuauReadWriteProperties) - prop = Property::create(replace(prop.readType()), replace(prop.writeType())); + if (FFlag::DebugLuauDeferredConstraintResolution) + prop = Property::create(replace(prop.readTy), replace(prop.writeTy)); else prop.setType(replace(prop.type())); } diff --git a/Analysis/src/Subtyping.cpp b/Analysis/src/Subtyping.cpp index 3c7c32f1..6abf9a3b 100644 --- a/Analysis/src/Subtyping.cpp +++ b/Analysis/src/Subtyping.cpp @@ -7,7 +7,9 @@ #include "Luau/Normalize.h" #include "Luau/Scope.h" #include "Luau/StringUtils.h" +#include "Luau/Substitution.h" #include "Luau/ToString.h" +#include "Luau/TxnLog.h" #include "Luau/Type.h" #include "Luau/TypeArena.h" #include "Luau/TypeCheckLimits.h" @@ -217,7 +219,14 @@ SubtypingResult& SubtypingResult::withSuperPath(TypePath::Path path) SubtypingResult& SubtypingResult::withErrors(ErrorVec& err) { - errors = std::move(err); + for (TypeError& e : err) + errors.emplace_back(e); + return *this; +} + +SubtypingResult& SubtypingResult::withError(TypeError err) +{ + errors.push_back(std::move(err)); return *this; } @@ -245,6 +254,74 @@ SubtypingResult SubtypingResult::any(const std::vector& results return acc; } +struct ApplyMappedGenerics : Substitution +{ + using MappedGenerics = DenseHashMap; + using MappedGenericPacks = DenseHashMap; + + NotNull builtinTypes; + NotNull arena; + + MappedGenerics& mappedGenerics; + MappedGenericPacks& mappedGenericPacks; + + + ApplyMappedGenerics(NotNull builtinTypes, NotNull arena, MappedGenerics& mappedGenerics, MappedGenericPacks& mappedGenericPacks) + : Substitution(TxnLog::empty(), arena) + , builtinTypes(builtinTypes) + , arena(arena) + , mappedGenerics(mappedGenerics) + , mappedGenericPacks(mappedGenericPacks) + { + } + + bool isDirty(TypeId ty) override + { + return mappedGenerics.contains(ty); + } + + bool isDirty(TypePackId tp) override + { + return mappedGenericPacks.contains(tp); + } + + TypeId clean(TypeId ty) override + { + const auto& bounds = mappedGenerics[ty]; + + if (bounds.upperBound.empty()) + return builtinTypes->unknownType; + + if (bounds.upperBound.size() == 1) + return *begin(bounds.upperBound); + + return arena->addType(IntersectionType{std::vector(begin(bounds.upperBound), end(bounds.upperBound))}); + } + + TypePackId clean(TypePackId tp) override + { + return mappedGenericPacks[tp]; + } + + bool ignoreChildren(TypeId ty) override + { + if (get(ty)) + return true; + + return ty->persistent; + } + bool ignoreChildren(TypePackId ty) override + { + return ty->persistent; + } +}; + +std::optional SubtypingEnvironment::applyMappedGenerics(NotNull builtinTypes, NotNull arena, TypeId ty) +{ + ApplyMappedGenerics amg{builtinTypes, arena, mappedGenerics, mappedGenericPacks}; + return amg.substitute(ty); +} + Subtyping::Subtyping(NotNull builtinTypes, NotNull typeArena, NotNull normalizer, NotNull iceReporter, NotNull scope) : builtinTypes(builtinTypes) @@ -493,9 +570,19 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypeId sub } } else if (auto subTypeFamilyInstance = get(subTy)) + { + if (auto substSubTy = env.applyMappedGenerics(builtinTypes, arena, subTy)) + subTypeFamilyInstance = get(*substSubTy); + result = isCovariantWith(env, subTypeFamilyInstance, superTy); + } else if (auto superTypeFamilyInstance = get(superTy)) + { + if (auto substSuperTy = env.applyMappedGenerics(builtinTypes, arena, superTy)) + superTypeFamilyInstance = get(*substSuperTy); + result = isCovariantWith(env, subTy, superTypeFamilyInstance); + } else if (auto subGeneric = get(subTy); subGeneric && variance == Variance::Covariant) { bool ok = bindGeneric(env, subTy, superTy); @@ -604,7 +691,9 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypePackId else if (get(*subTail)) return SubtypingResult{true}.withSubComponent(TypePath::PackField::Tail); else - unexpected(*subTail); + return SubtypingResult{false} + .withSubComponent(TypePath::PackField::Tail) + .withError({scope->location, UnexpectedTypePackInSubtyping{*subTail}}); } else { @@ -656,7 +745,9 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypePackId else if (get(*superTail)) return SubtypingResult{true}.withSuperComponent(TypePath::PackField::Tail); else - unexpected(*superTail); + return SubtypingResult{false} + .withSuperComponent(TypePath::PackField::Tail) + .withError({scope->location, UnexpectedTypePackInSubtyping{*subTail}}); } else return {false}; @@ -717,8 +808,10 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypePackId // error type is fine on either side results.push_back(SubtypingResult{true}.withBothComponent(TypePath::PackField::Tail)); else - iceReporter->ice( - format("Subtyping::isSubtype got unexpected type packs %s and %s", toString(*subTail).c_str(), toString(*superTail).c_str())); + return SubtypingResult{false} + .withBothComponent(TypePath::PackField::Tail) + .withError({scope->location, UnexpectedTypePackInSubtyping{*subTail}}) + .withError({scope->location, UnexpectedTypePackInSubtyping{*superTail}}); } else if (subTail) { @@ -732,7 +825,9 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypePackId return SubtypingResult{ok}.withSubComponent(TypePath::PackField::Tail); } else - unexpected(*subTail); + return SubtypingResult{false} + .withSubComponent(TypePath::PackField::Tail) + .withError({scope->location, UnexpectedTypePackInSubtyping{*subTail}}); } else if (superTail) { @@ -759,7 +854,9 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypePackId results.push_back(SubtypingResult{false}.withSuperComponent(TypePath::PackField::Tail)); } else - iceReporter->ice("Subtyping test encountered the unexpected type pack: " + toString(*superTail)); + return SubtypingResult{false} + .withSuperComponent(TypePath::PackField::Tail) + .withError({scope->location, UnexpectedTypePackInSubtyping{*superTail}}); } SubtypingResult result = SubtypingResult::all(results); @@ -1126,18 +1223,32 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const Tabl if (subTable->props.empty() && !subTable->indexer && superTable->indexer) return {false}; - for (const auto& [name, prop] : superTable->props) + for (const auto& [name, superProp] : superTable->props) { std::vector results; - if (auto it = subTable->props.find(name); it != subTable->props.end()) - results.push_back(isInvariantWith(env, it->second.type(), prop.type()).withBothComponent(TypePath::Property(name))); + if (auto subIter = subTable->props.find(name); subIter != subTable->props.end()) + results.push_back(isCovariantWith(env, subIter->second, superProp, name)); if (subTable->indexer) { - if (isInvariantWith(env, subTable->indexer->indexType, builtinTypes->stringType).isSubtype) - results.push_back(isInvariantWith(env, subTable->indexer->indexResultType, prop.type()) - .withSubComponent(TypePath::TypeField::IndexResult) - .withSuperComponent(TypePath::Property(name))); + if (isCovariantWith(env, builtinTypes->stringType, subTable->indexer->indexType).isSubtype) + { + if (superProp.isShared()) + results.push_back(isInvariantWith(env, subTable->indexer->indexResultType, superProp.type()) + .withSubComponent(TypePath::TypeField::IndexResult) + .withSuperComponent(TypePath::Property::read(name))); + else + { + if (superProp.readTy) + results.push_back(isCovariantWith(env, subTable->indexer->indexResultType, *superProp.readTy) + .withSubComponent(TypePath::TypeField::IndexResult) + .withSuperComponent(TypePath::Property::read(name))); + if (superProp.writeTy) + results.push_back(isContravariantWith(env, subTable->indexer->indexResultType, *superProp.writeTy) + .withSubComponent(TypePath::TypeField::IndexResult) + .withSuperComponent(TypePath::Property::write(name))); + } + } } if (results.empty()) @@ -1197,7 +1308,9 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const Clas for (const auto& [name, prop] : superTable->props) { if (auto classProp = lookupClassProp(subClass, name)) - result.andAlso(isInvariantWith(env, prop.type(), classProp->type()).withBothComponent(TypePath::Property(name))); + { + result.andAlso(isCovariantWith(env, *classProp, prop, name)); + } else return SubtypingResult{false}; } @@ -1230,7 +1343,7 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const Prim { if (auto stringTable = get(it->second.type())) result.orElse( - isCovariantWith(env, stringTable, superTable).withSubPath(TypePath::PathBuilder().mt().prop("__index").build())); + isCovariantWith(env, stringTable, superTable).withSubPath(TypePath::PathBuilder().mt().readProp("__index").build())); } } } @@ -1252,7 +1365,7 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const Sing { if (auto stringTable = get(it->second.type())) result.orElse( - isCovariantWith(env, stringTable, superTable).withSubPath(TypePath::PathBuilder().mt().prop("__index").build())); + isCovariantWith(env, stringTable, superTable).withSubPath(TypePath::PathBuilder().mt().readProp("__index").build())); } } } @@ -1267,6 +1380,31 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const Tabl .andAlso(isInvariantWith(env, subIndexer.indexResultType, superIndexer.indexResultType).withBothComponent(TypePath::TypeField::IndexResult)); } +SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const Property& subProp, const Property& superProp, const std::string& name) +{ + SubtypingResult res{true}; + + if (superProp.isShared() && subProp.isShared()) + res.andAlso(isInvariantWith(env, subProp.type(), superProp.type()).withBothComponent(TypePath::Property::read(name))); + else + { + if (superProp.readTy.has_value() && subProp.readTy.has_value()) + res.andAlso(isCovariantWith(env, *subProp.readTy, *superProp.readTy).withBothComponent(TypePath::Property::read(name))); + if (superProp.writeTy.has_value() && subProp.writeTy.has_value()) + res.andAlso(isContravariantWith(env, *subProp.writeTy, *superProp.writeTy).withBothComponent(TypePath::Property::write(name))); + + if (superProp.isReadWrite()) + { + if (subProp.isReadOnly()) + res.andAlso(SubtypingResult{false}.withBothComponent(TypePath::Property::read(name))); + else if (subProp.isWriteOnly()) + res.andAlso(SubtypingResult{false}.withBothComponent(TypePath::Property::write(name))); + } + } + + return res; +} + SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const NormalizedType* subNorm, const NormalizedType* superNorm) { if (!subNorm || !superNorm) @@ -1473,14 +1611,4 @@ TypeId Subtyping::makeAggregateType(const Container& container, TypeId orElse) return arena->addType(T{std::vector(begin(container), end(container))}); } -void Subtyping::unexpected(TypeId ty) -{ - iceReporter->ice(format("Unexpected type %s", toString(ty).c_str())); -} - -void Subtyping::unexpected(TypePackId tp) -{ - iceReporter->ice(format("Unexpected type pack %s", toString(tp).c_str())); -} - } // namespace Luau diff --git a/Analysis/src/ToString.cpp b/Analysis/src/ToString.cpp index 2e204926..e26ed138 100644 --- a/Analysis/src/ToString.cpp +++ b/Analysis/src/ToString.cpp @@ -374,7 +374,7 @@ struct TypeStringifier tv->ty); } - void stringify(const std::string& name, const Property& prop) + void emitKey(const std::string& name) { if (isIdentifier(name)) state.emit(name); @@ -385,31 +385,46 @@ struct TypeStringifier state.emit("\"]"); } state.emit(": "); + } - if (FFlag::DebugLuauReadWriteProperties) + void _newStringify(const std::string& name, const Property& prop) + { + bool comma = false; + if (prop.isShared()) { - // We special case the stringification if the property's read and write types are shared. - if (prop.isShared()) - return stringify(*prop.readType()); - - // Otherwise emit them separately. - if (auto ty = prop.readType()) - { - state.emit("read "); - stringify(*ty); - } - - if (prop.readType() && prop.writeType()) - state.emit(" + "); - - if (auto ty = prop.writeType()) - { - state.emit("write "); - stringify(*ty); - } - } - else + emitKey(name); stringify(prop.type()); + return; + } + + if (prop.readTy) + { + state.emit("read "); + emitKey(name); + stringify(*prop.readTy); + comma = true; + } + if (prop.writeTy) + { + if (comma) + { + state.emit(","); + state.newline(); + } + + state.emit("write "); + emitKey(name); + stringify(*prop.writeTy); + } + } + + void stringify(const std::string& name, const Property& prop) + { + if (FFlag::DebugLuauDeferredConstraintResolution) + return _newStringify(name, prop); + + emitKey(name); + stringify(prop.type()); } void stringify(TypePackId tp); @@ -1755,7 +1770,7 @@ std::string toString(const Constraint& constraint, ToStringOptions& opts) } else if constexpr (std::is_same_v) { - return tos(c.resultType) + " ~ hasProp " + tos(c.subjectType) + ", \"" + c.prop + "\""; + return tos(c.resultType) + " ~ hasProp " + tos(c.subjectType) + ", \"" + c.prop + "\" ctx=" + std::to_string(int(c.context)); } else if constexpr (std::is_same_v) { @@ -1801,6 +1816,8 @@ std::string toString(const Constraint& constraint, ToStringOptions& opts) { return "reduce " + tos(c.tp); } + else if constexpr (std::is_same_v) + return "equality: " + tos(c.resultType) + " ~ " + tos(c.assignmentType); else static_assert(always_false_v, "Non-exhaustive constraint switch"); }; diff --git a/Analysis/src/Type.cpp b/Analysis/src/Type.cpp index 71a07d73..d8044c16 100644 --- a/Analysis/src/Type.cpp +++ b/Analysis/src/Type.cpp @@ -26,7 +26,6 @@ LUAU_FASTINTVARIABLE(LuauTypeMaximumStringifierLength, 500) LUAU_FASTINTVARIABLE(LuauTableTypeMaximumStringifierLength, 0) LUAU_FASTINT(LuauTypeInferRecursionLimit) LUAU_FASTFLAG(LuauInstantiateInSubtyping) -LUAU_FASTFLAG(DebugLuauReadWriteProperties) namespace Luau { @@ -632,13 +631,10 @@ Property::Property(TypeId readTy, bool deprecated, const std::string& deprecated , readTy(readTy) , writeTy(readTy) { - LUAU_ASSERT(!FFlag::DebugLuauReadWriteProperties); } Property Property::readonly(TypeId ty) { - LUAU_ASSERT(FFlag::DebugLuauReadWriteProperties); - Property p; p.readTy = ty; return p; @@ -646,8 +642,6 @@ Property Property::readonly(TypeId ty) Property Property::writeonly(TypeId ty) { - LUAU_ASSERT(FFlag::DebugLuauReadWriteProperties); - Property p; p.writeTy = ty; return p; @@ -660,8 +654,6 @@ Property Property::rw(TypeId ty) Property Property::rw(TypeId read, TypeId write) { - LUAU_ASSERT(FFlag::DebugLuauReadWriteProperties); - Property p; p.readTy = read; p.writeTy = write; @@ -683,29 +675,15 @@ Property Property::create(std::optional read, std::optional writ TypeId Property::type() const { - LUAU_ASSERT(!FFlag::DebugLuauReadWriteProperties); LUAU_ASSERT(readTy); return *readTy; } void Property::setType(TypeId ty) { - LUAU_ASSERT(!FFlag::DebugLuauReadWriteProperties); readTy = ty; -} - -std::optional Property::readType() const -{ - LUAU_ASSERT(FFlag::DebugLuauReadWriteProperties); - LUAU_ASSERT(!(bool(readTy) && bool(writeTy))); - return readTy; -} - -std::optional Property::writeType() const -{ - LUAU_ASSERT(FFlag::DebugLuauReadWriteProperties); - LUAU_ASSERT(!(bool(readTy) && bool(writeTy))); - return writeTy; + if (FFlag::DebugLuauDeferredConstraintResolution) + writeTy = ty; } bool Property::isShared() const @@ -713,6 +691,21 @@ bool Property::isShared() const return readTy && writeTy && readTy == writeTy; } +bool Property::isReadOnly() const +{ + return readTy && !writeTy; +} + +bool Property::isWriteOnly() const +{ + return !readTy && writeTy; +} + +bool Property::isReadWrite() const +{ + return readTy && writeTy; +} + TableType::TableType(TableState state, TypeLevel level, Scope* scope) : state(state) , level(level) @@ -961,6 +954,7 @@ BuiltinTypes::BuiltinTypes() , optionalStringType(arena->addType(Type{UnionType{{stringType, nilType}}, /*persistent*/ true})) , emptyTypePack(arena->addTypePack(TypePackVar{TypePack{{}}, /*persistent*/ true})) , anyTypePack(arena->addTypePack(TypePackVar{VariadicTypePack{anyType}, /*persistent*/ true})) + , unknownTypePack(arena->addTypePack(TypePackVar{VariadicTypePack{unknownType}, /*persistent*/ true})) , neverTypePack(arena->addTypePack(TypePackVar{VariadicTypePack{neverType}, /*persistent*/ true})) , uninhabitableTypePack(arena->addTypePack(TypePackVar{TypePack{{neverType}, neverTypePack}, /*persistent*/ true})) , errorTypePack(arena->addTypePack(TypePackVar{Unifiable::Error{}, /*persistent*/ true})) diff --git a/Analysis/src/TypeChecker2.cpp b/Analysis/src/TypeChecker2.cpp index 5411bf24..853e46ff 100644 --- a/Analysis/src/TypeChecker2.cpp +++ b/Analysis/src/TypeChecker2.cpp @@ -1460,7 +1460,7 @@ struct TypeChecker2 { visit(expr, ValueContext::RValue); TypeId leftType = stripFromNilAndReport(lookupType(expr), location); - checkIndexTypeFromType(leftType, propName, location, context, astIndexExprTy); + checkIndexTypeFromType(leftType, propName, context, location, astIndexExprTy); } void visit(AstExprIndexName* indexName, ValueContext context) @@ -1709,8 +1709,8 @@ struct TypeChecker2 TypeId visit(AstExprBinary* expr, AstNode* overrideKey = nullptr) { - visit(expr->left, ValueContext::LValue); - visit(expr->right, ValueContext::LValue); + visit(expr->left, ValueContext::RValue); + visit(expr->right, ValueContext::RValue); NotNull scope = stack.back(); @@ -2534,20 +2534,16 @@ struct TypeChecker2 reportError(std::move(e)); } - // If the provided type does not have the named property, report an error. - void checkIndexTypeFromType(TypeId tableTy, const std::string& prop, const Location& location, ValueContext context, TypeId astIndexExprType) + /* A helper for checkIndexTypeFromType. + * + * Returns a pair: + * * A boolean indicating that at least one of the constituent types + * contains the prop, and + * * A vector of types that do not contain the prop. + */ + std::pair> lookupProp(const NormalizedType* norm, const std::string& prop, ValueContext context, + const Location& location, TypeId astIndexExprType, std::vector& errors) { - const NormalizedType* norm = normalizer.normalize(tableTy); - if (!norm) - { - reportError(NormalizationTooComplex{}, location); - return; - } - - // if the type is error suppressing, we don't actually have any work left to do. - if (norm->shouldSuppressErrors()) - return; - bool foundOneProp = false; std::vector typesMissingTheProp; @@ -2556,7 +2552,7 @@ struct TypeChecker2 return; DenseHashSet seen{nullptr}; - bool found = hasIndexTypeFromType(ty, prop, location, seen, astIndexExprType); + bool found = hasIndexTypeFromType(ty, prop, context, location, seen, astIndexExprType, errors); foundOneProp |= found; if (!found) typesMissingTheProp.push_back(ty); @@ -2601,6 +2597,26 @@ struct TypeChecker2 fetch(tyvar); } + return {foundOneProp, typesMissingTheProp}; + } + + // If the provided type does not have the named property, report an error. + void checkIndexTypeFromType(TypeId tableTy, const std::string& prop, ValueContext context, const Location& location, TypeId astIndexExprType) + { + const NormalizedType* norm = normalizer.normalize(tableTy); + if (!norm) + { + reportError(NormalizationTooComplex{}, location); + return; + } + + // if the type is error suppressing, we don't actually have any work left to do. + if (norm->shouldSuppressErrors()) + return; + + std::vector dummy; + const auto [foundOneProp, typesMissingTheProp] = lookupProp(norm, prop, context, location, astIndexExprType, module->errors); + if (!typesMissingTheProp.empty()) { if (foundOneProp) @@ -2611,17 +2627,29 @@ struct TypeChecker2 // the `else` branch. else if (context == ValueContext::LValue && !get(tableTy)) { - if (get(tableTy) || get(tableTy)) + const auto [lvFoundOneProp, lvTypesMissingTheProp] = lookupProp(norm, prop, ValueContext::RValue, location, astIndexExprType, dummy); + if (lvFoundOneProp && lvTypesMissingTheProp.empty()) + reportError(PropertyAccessViolation{tableTy, prop, PropertyAccessViolation::CannotWrite}, location); + else if (get(tableTy) || get(tableTy)) reportError(NotATable{tableTy}, location); else reportError(CannotExtendTable{tableTy, CannotExtendTable::Property, prop}, location); } + else if (context == ValueContext::RValue && !get(tableTy)) + { + const auto [rvFoundOneProp, rvTypesMissingTheProp] = lookupProp(norm, prop, ValueContext::LValue, location, astIndexExprType, dummy); + if (rvFoundOneProp && rvTypesMissingTheProp.empty()) + reportError(PropertyAccessViolation{tableTy, prop, PropertyAccessViolation::CannotRead}, location); + else + reportError(UnknownProperty{tableTy, prop}, location); + } else reportError(UnknownProperty{tableTy, prop}, location); } } - bool hasIndexTypeFromType(TypeId ty, const std::string& prop, const Location& location, DenseHashSet& seen, TypeId astIndexExprType) + bool hasIndexTypeFromType(TypeId ty, const std::string& prop, ValueContext context, const Location& location, DenseHashSet& seen, + TypeId astIndexExprType, std::vector& errors) { // If we have already encountered this type, we must assume that some // other codepath will do the right thing and signal false if the @@ -2635,14 +2663,14 @@ struct TypeChecker2 if (isString(ty)) { - std::optional mtIndex = Luau::findMetatableEntry(builtinTypes, module->errors, builtinTypes->stringType, "__index", location); + std::optional mtIndex = Luau::findMetatableEntry(builtinTypes, errors, builtinTypes->stringType, "__index", location); LUAU_ASSERT(mtIndex); ty = *mtIndex; } if (auto tt = getTableType(ty)) { - if (findTablePropertyRespectingMeta(builtinTypes, module->errors, ty, prop, location)) + if (findTablePropertyRespectingMeta(builtinTypes, errors, ty, prop, context, location)) return true; if (tt->indexer) @@ -2674,11 +2702,11 @@ struct TypeChecker2 } else if (const UnionType* utv = get(ty)) return std::all_of(begin(utv), end(utv), [&](TypeId part) { - return hasIndexTypeFromType(part, prop, location, seen, astIndexExprType); + return hasIndexTypeFromType(part, prop, context, location, seen, astIndexExprType, errors); }); else if (const IntersectionType* itv = get(ty)) return std::any_of(begin(itv), end(itv), [&](TypeId part) { - return hasIndexTypeFromType(part, prop, location, seen, astIndexExprType); + return hasIndexTypeFromType(part, prop, context, location, seen, astIndexExprType, errors); }); else return false; diff --git a/Analysis/src/TypeFamily.cpp b/Analysis/src/TypeFamily.cpp index 9d28e7da..5456c423 100644 --- a/Analysis/src/TypeFamily.cpp +++ b/Analysis/src/TypeFamily.cpp @@ -7,6 +7,7 @@ #include "Luau/DenseHash.h" #include "Luau/Instantiation.h" #include "Luau/Normalize.h" +#include "Luau/NotNull.h" #include "Luau/Simplify.h" #include "Luau/Substitution.h" #include "Luau/Subtyping.h" @@ -853,6 +854,27 @@ static TypeFamilyReductionResult comparisonFamilyFn(TypeId instance, con TypeId lhsTy = follow(typeParams.at(0)); TypeId rhsTy = follow(typeParams.at(1)); + // Algebra Reduction Rules for comparison family functions + // Note that comparing to never tells you nothing about the other operand + // lt< 'a , never> -> continue + // lt< never, 'a> -> continue + // lt< 'a, t> -> 'a is t - we'll solve the constraint, return and solve lt -> bool + // lt< t, 'a> -> same as above + bool canSubmitConstraint = ctx->solver && ctx->constraint; + if (canSubmitConstraint) + { + if (get(lhsTy) && get(rhsTy) == nullptr) + { + auto c1 = ctx->solver->pushConstraint(ctx->scope, {}, EqualityConstraint{lhsTy, rhsTy}); + const_cast(ctx->constraint)->dependencies.emplace_back(c1); + } + else if (get(rhsTy) && get(lhsTy) == nullptr) + { + auto c1 = ctx->solver->pushConstraint(ctx->scope, {}, EqualityConstraint{rhsTy, lhsTy}); + const_cast(ctx->constraint)->dependencies.emplace_back(c1); + } + } + // check to see if both operand types are resolved enough, and wait to reduce if not if (isPending(lhsTy, ctx->solver)) return {std::nullopt, false, {lhsTy}, {}}; @@ -1248,8 +1270,8 @@ TypeFamilyReductionResult keyofFamilyImpl( if (!normTy) return {std::nullopt, false, {}, {}}; - // if we don't have either just tables or just classes, we've got nothing to get keys of (at least until a future version perhaps adds classes as - // well) + // if we don't have either just tables or just classes, we've got nothing to get keys of (at least until a future version perhaps adds classes + // as well) if (normTy->hasTables() == normTy->hasClasses()) return {std::nullopt, true, {}, {}}; diff --git a/Analysis/src/TypePath.cpp b/Analysis/src/TypePath.cpp index fb4d68cb..50507263 100644 --- a/Analysis/src/TypePath.cpp +++ b/Analysis/src/TypePath.cpp @@ -13,7 +13,7 @@ #include #include -LUAU_FASTFLAG(DebugLuauReadWriteProperties); +LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution); // Maximum number of steps to follow when traversing a path. May not always // equate to the number of components in a path, depending on the traversal @@ -29,7 +29,7 @@ namespace TypePath Property::Property(std::string name) : name(std::move(name)) { - LUAU_ASSERT(!FFlag::DebugLuauReadWriteProperties); + LUAU_ASSERT(!FFlag::DebugLuauDeferredConstraintResolution); } Property Property::read(std::string name) @@ -146,21 +146,21 @@ Path PathBuilder::build() PathBuilder& PathBuilder::readProp(std::string name) { - LUAU_ASSERT(FFlag::DebugLuauReadWriteProperties); + LUAU_ASSERT(FFlag::DebugLuauDeferredConstraintResolution); components.push_back(Property{std::move(name), true}); return *this; } PathBuilder& PathBuilder::writeProp(std::string name) { - LUAU_ASSERT(FFlag::DebugLuauReadWriteProperties); + LUAU_ASSERT(FFlag::DebugLuauDeferredConstraintResolution); components.push_back(Property{std::move(name), false}); return *this; } PathBuilder& PathBuilder::prop(std::string name) { - LUAU_ASSERT(!FFlag::DebugLuauReadWriteProperties); + LUAU_ASSERT(!FFlag::DebugLuauDeferredConstraintResolution); components.push_back(Property{std::move(name)}); return *this; } @@ -323,7 +323,7 @@ struct TraversalState // logic there. updateCurrent(*m); - if (!traverse(TypePath::Property{"__index"})) + if (!traverse(TypePath::Property::read("__index"))) return false; return traverse(property); @@ -333,8 +333,8 @@ struct TraversalState if (prop) { std::optional maybeType; - if (FFlag::DebugLuauReadWriteProperties) - maybeType = property.isRead ? prop->readType() : prop->writeType(); + if (FFlag::DebugLuauDeferredConstraintResolution) + maybeType = property.isRead ? prop->readTy : prop->writeTy; else maybeType = prop->type(); @@ -514,7 +514,7 @@ std::string toString(const TypePath::Path& path, bool prefixDot) if constexpr (std::is_same_v) { result << '['; - if (FFlag::DebugLuauReadWriteProperties) + if (FFlag::DebugLuauDeferredConstraintResolution) { if (c.isRead) result << "read "; diff --git a/Analysis/src/TypeUtils.cpp b/Analysis/src/TypeUtils.cpp index f746f620..795324f4 100644 --- a/Analysis/src/TypeUtils.cpp +++ b/Analysis/src/TypeUtils.cpp @@ -44,6 +44,12 @@ std::optional findMetatableEntry( std::optional findTablePropertyRespectingMeta( NotNull builtinTypes, ErrorVec& errors, TypeId ty, const std::string& name, Location location) +{ + return findTablePropertyRespectingMeta(builtinTypes, errors, ty, name, ValueContext::RValue, location); +} + +std::optional findTablePropertyRespectingMeta( + NotNull builtinTypes, ErrorVec& errors, TypeId ty, const std::string& name, ValueContext context, Location location) { if (get(ty)) return ty; @@ -52,7 +58,20 @@ std::optional findTablePropertyRespectingMeta( { const auto& it = tableType->props.find(name); if (it != tableType->props.end()) - return it->second.type(); + { + if (FFlag::DebugLuauDeferredConstraintResolution) + { + switch (context) + { + case ValueContext::RValue: + return it->second.readTy; + case ValueContext::LValue: + return it->second.writeTy; + } + } + else + return it->second.type(); + } } std::optional mtIndex = findMetatableEntry(builtinTypes, errors, ty, "__index", location); diff --git a/Analysis/src/Unifier2.cpp b/Analysis/src/Unifier2.cpp index 5d3dc864..02e2bf67 100644 --- a/Analysis/src/Unifier2.cpp +++ b/Analysis/src/Unifier2.cpp @@ -8,6 +8,7 @@ #include "Luau/Type.h" #include "Luau/TypeArena.h" #include "Luau/TypeCheckLimits.h" +#include "Luau/TypeFamily.h" #include "Luau/TypeUtils.h" #include "Luau/VisitType.h" @@ -19,6 +20,59 @@ LUAU_FASTINT(LuauTypeInferRecursionLimit) namespace Luau { +static bool areCompatible(TypeId left, TypeId right) +{ + auto p = get2(follow(left), follow(right)); + if (!p) + return true; + + const TableType* leftTable = p.first; + LUAU_ASSERT(leftTable); + const TableType* rightTable = p.second; + LUAU_ASSERT(rightTable); + + const auto missingPropIsCompatible = [](const Property& leftProp, const TableType* rightTable) { + // Two tables may be compatible even if their shapes aren't exactly the + // same if the extra property is optional, free (and therefore + // potentially optional), or if the right table has an indexer. Or if + // the right table is free (and therefore potentially has an indexer or + // a compatible property) + + LUAU_ASSERT(leftProp.isReadOnly() || leftProp.isShared()); + + const TypeId leftType = follow( + leftProp.isReadOnly() ? *leftProp.readTy : leftProp.type() + ); + + if (isOptional(leftType) || get(leftType) || rightTable->state == TableState::Free || rightTable->indexer.has_value()) + return true; + + return false; + }; + + for (const auto& [name, leftProp]: leftTable->props) + { + auto it = rightTable->props.find(name); + if (it == rightTable->props.end()) + { + if (!missingPropIsCompatible(leftProp, rightTable)) + return false; + } + } + + for (const auto& [name, rightProp]: rightTable->props) + { + auto it = leftTable->props.find(name); + if (it == leftTable->props.end()) + { + if (!missingPropIsCompatible(rightProp, leftTable)) + return false; + } + } + + return true; +} + Unifier2::Unifier2(NotNull arena, NotNull builtinTypes, NotNull scope, NotNull ice) : arena(arena) , builtinTypes(builtinTypes) @@ -34,6 +88,12 @@ bool Unifier2::unify(TypeId subTy, TypeId superTy) subTy = follow(subTy); superTy = follow(superTy); + if (auto subGen = genericSubstitutions.find(subTy)) + return unify(*subGen, superTy); + + if (auto superGen = genericSubstitutions.find(superTy)) + return unify(subTy, *superGen); + if (seenTypePairings.contains({subTy, superTy})) return true; seenTypePairings.insert({subTy, superTy}); @@ -44,14 +104,21 @@ bool Unifier2::unify(TypeId subTy, TypeId superTy) FreeType* subFree = getMutable(subTy); FreeType* superFree = getMutable(superTy); - if (subFree) + if (subFree && superFree) + { + superFree->lowerBound = mkUnion(subFree->lowerBound, superFree->lowerBound); + superFree->upperBound = mkIntersection(subFree->upperBound, superFree->upperBound); + asMutable(subTy)->ty.emplace(superTy); + } + else if (subFree) { subFree->upperBound = mkIntersection(subFree->upperBound, superTy); expandedFreeTypes[subTy].push_back(superTy); } - - if (superFree) + else if (superFree) + { superFree->lowerBound = mkUnion(superFree->lowerBound, subTy); + } if (subFree || superFree) return true; @@ -159,13 +226,11 @@ bool Unifier2::unify(TypeId subTy, const FunctionType* superFn) if (shouldInstantiate) { - std::optional instantiated = instantiate(builtinTypes, arena, NotNull{&limits}, scope, subTy); - if (!instantiated) - return false; + for (auto generic : subFn->generics) + genericSubstitutions[generic] = freshType(arena, builtinTypes, scope); - subFn = get(*instantiated); - - LUAU_ASSERT(subFn); // instantiation should not make a function type _not_ a function type. + for (auto genericPack : subFn->genericPacks) + genericPackSubstitutions[genericPack] = arena->freshTypePack(scope); } bool argResult = unify(superFn->argTypes, subFn->argTypes); @@ -179,7 +244,10 @@ bool Unifier2::unify(const UnionType* subUnion, TypeId superTy) // if the occurs check fails for any option, it fails overall for (auto subOption : subUnion->options) - result &= unify(subOption, superTy); + { + if (areCompatible(subOption, superTy)) + result &= unify(subOption, superTy); + } return result; } @@ -190,7 +258,10 @@ bool Unifier2::unify(TypeId subTy, const UnionType* superUnion) // if the occurs check fails for any option, it fails overall for (auto superOption : superUnion->options) - result &= unify(subTy, superOption); + { + if (areCompatible(subTy, superOption)) + result &= unify(subTy, superOption); + } return result; } @@ -228,7 +299,21 @@ bool Unifier2::unify(TableType* subTable, const TableType* superTable) auto superPropOpt = superTable->props.find(propName); if (superPropOpt != superTable->props.end()) - result &= unify(subProp.type(), superPropOpt->second.type()); + { + const Property& superProp = superPropOpt->second; + + if (subProp.isReadOnly() && superProp.isReadOnly()) + result &= unify(*subProp.readTy, *superPropOpt->second.readTy); + else if (subProp.isReadOnly()) + result &= unify(*subProp.readTy, superProp.type()); + else if (superProp.isReadOnly()) + result &= unify(subProp.type(), *superProp.readTy); + else + { + result &= unify(subProp.type(), superProp.type()); + result &= unify(superProp.type(), subProp.type()); + } + } } auto subTypeParamsIter = subTable->instantiatedTypeParams.begin(); @@ -293,10 +378,19 @@ bool Unifier2::unify(TypePackId subTp, TypePackId superTp) subTp = follow(subTp); superTp = follow(superTp); + if (auto subGen = genericPackSubstitutions.find(subTp)) + return unify(*subGen, superTp); + + if (auto superGen = genericPackSubstitutions.find(superTp)) + return unify(subTp, *superGen); + if (seenTypePackPairings.contains({subTp, superTp})) return true; seenTypePackPairings.insert({subTp, superTp}); + if (subTp == superTp) + return true; + const FreeTypePack* subFree = get(subTp); const FreeTypePack* superFree = get(superTp); @@ -378,11 +472,14 @@ struct FreeTypeSearcher : TypeVisitor { } - enum + enum Polarity { Positive, - Negative - } polarity = Positive; + Negative, + Both, + }; + + Polarity polarity = Positive; void flip() { @@ -394,6 +491,8 @@ struct FreeTypeSearcher : TypeVisitor case Negative: polarity = Positive; break; + case Both: + break; } } @@ -419,6 +518,10 @@ struct FreeTypeSearcher : TypeVisitor case Negative: negativeTypes[ty]++; break; + case Both: + positiveTypes[ty]++; + negativeTypes[ty]++; + break; } return true; @@ -436,10 +539,35 @@ struct FreeTypeSearcher : TypeVisitor case Negative: negativeTypes[ty]++; break; + case Both: + positiveTypes[ty]++; + negativeTypes[ty]++; + break; } } - return true; + for (const auto& [_name, prop] : tt.props) + { + if (prop.isReadOnly()) + traverse(*prop.readTy); + else + { + LUAU_ASSERT(prop.isShared()); + + Polarity p = polarity; + polarity = Both; + traverse(prop.type()); + polarity = p; + } + } + + if (tt.indexer) + { + traverse(tt.indexer->indexType); + traverse(tt.indexer->indexResultType); + } + + return false; } bool visit(TypeId ty, const FunctionType& ft) override @@ -538,8 +666,8 @@ struct MutatingGeneralizer : TypeOnceVisitor if (!ft) return false; - const bool positiveCount = getCount(positiveTypes, ty); - const bool negativeCount = getCount(negativeTypes, ty); + const size_t positiveCount = getCount(positiveTypes, ty); + const size_t negativeCount = getCount(negativeTypes, ty); if (!positiveCount && !negativeCount) return false; diff --git a/Ast/src/Parser.cpp b/Ast/src/Parser.cpp index fa1ab61e..a7363552 100644 --- a/Ast/src/Parser.cpp +++ b/Ast/src/Parser.cpp @@ -18,6 +18,7 @@ LUAU_FASTINTVARIABLE(LuauParseErrorLimit, 100) // See docs/SyntaxChanges.md for an explanation. LUAU_FASTFLAG(LuauCheckedFunctionSyntax) LUAU_FASTFLAGVARIABLE(LuauReadWritePropertySyntax, false) +LUAU_FASTFLAGVARIABLE(DebugLuauDeferredConstraintResolution, false) namespace Luau { @@ -1339,7 +1340,7 @@ AstType* Parser::parseTableType(bool inDeclarationContext) AstTableAccess access = AstTableAccess::ReadWrite; std::optional accessLocation; - if (FFlag::LuauReadWritePropertySyntax) + if (FFlag::LuauReadWritePropertySyntax || FFlag::DebugLuauDeferredConstraintResolution) { if (lexer.current().type == Lexeme::Name && lexer.lookahead().type != ':') { diff --git a/CLI/FileUtils.cpp b/CLI/FileUtils.cpp index 548d09b9..2e17bcb5 100644 --- a/CLI/FileUtils.cpp +++ b/CLI/FileUtils.cpp @@ -431,6 +431,10 @@ std::vector getSourceFiles(int argc, char** argv) for (int i = 1; i < argc; ++i) { + // Early out once we reach --program-args,-a since the remaining args are passed to lua + if (strcmp(argv[i], "--program-args") == 0 || strcmp(argv[i], "-a") == 0) + return files; + // Treat '-' as a special file whose source is read from stdin // All other arguments that start with '-' are skipped if (argv[i][0] == '-' && argv[i][1] != '\0') diff --git a/CLI/Reduce.cpp b/CLI/Reduce.cpp index 721bb51c..4b41fc76 100644 --- a/CLI/Reduce.cpp +++ b/CLI/Reduce.cpp @@ -152,7 +152,7 @@ struct Reducer } #if VERBOSE >= 1 - printf("running %s\n", command.c_str()); + printf("running %s\n", cmd.c_str()); #endif TestResult result = TestResult::NoBug; @@ -160,7 +160,7 @@ struct Reducer ++step; printf("Step %4d...\n", step); - FILE* p = popen(command.c_str(), "r"); + FILE* p = popen(cmd.c_str(), "r"); while (!feof(p)) { diff --git a/CLI/Repl.cpp b/CLI/Repl.cpp index 2e5cb065..84e4a654 100644 --- a/CLI/Repl.cpp +++ b/CLI/Repl.cpp @@ -46,6 +46,8 @@ LUAU_FASTFLAGVARIABLE(LuauUpdatedRequireByStringSemantics, false) constexpr int MaxTraversalLimit = 50; static bool codegen = false; +static int program_argc = 0; +char** program_argv = nullptr; // Ctrl-C handling static void sigintCallback(lua_State* L, int gc) @@ -318,6 +320,12 @@ void setupState(lua_State* L) luaL_sandbox(L); } +void setupArguments(lua_State* L, int argc, char** argv) +{ + for (int i = 0; i < argc; ++i) + lua_pushstring(L, argv[i]); +} + std::string runCode(lua_State* L, const std::string& source) { std::string bytecode = Luau::compile(source, copts()); @@ -668,7 +676,8 @@ static bool runFile(const char* name, lua_State* GL, bool repl) if (coverageActive()) coverageTrack(L, -1); - status = lua_resume(L, NULL, 0); + setupArguments(L, program_argc, program_argv); + status = lua_resume(L, NULL, program_argc); } else { @@ -704,7 +713,7 @@ static bool runFile(const char* name, lua_State* GL, bool repl) static void displayHelp(const char* argv0) { - printf("Usage: %s [options] [file list]\n", argv0); + printf("Usage: %s [options] [file list] [-a] [arg list]\n", argv0); printf("\n"); printf("When file list is omitted, an interactive REPL is started instead.\n"); printf("\n"); @@ -717,6 +726,7 @@ static void displayHelp(const char* argv0) printf(" --profile[=N]: profile the code using N Hz sampling (default 10000) and output results to profile.out\n"); printf(" --timetrace: record compiler time tracing information into trace.json\n"); printf(" --codegen: execute code using native code generation\n"); + printf(" --program-args,-a: declare start of arguments to be passed to the Luau program"); } static int assertionHandler(const char* expr, const char* file, int line, const char* function) @@ -739,6 +749,7 @@ int replMain(int argc, char** argv) bool coverage = false; bool interactive = false; bool codegenPerf = false; + int program_args = argc; for (int i = 1; i < argc; i++) { @@ -800,6 +811,11 @@ int replMain(int argc, char** argv) { setLuauFlags(argv[i] + 9); } + else if (strcmp(argv[i], "--program-args") == 0 || strcmp(argv[i], "-a") == 0) + { + program_args = i + 1; + break; + } else if (argv[i][0] == '-') { fprintf(stderr, "Error: Unrecognized option '%s'.\n\n", argv[i]); @@ -808,6 +824,10 @@ int replMain(int argc, char** argv) } } + program_argc = argc - program_args; + program_argv = &argv[program_args]; + + #if !defined(LUAU_ENABLE_TIME_TRACE) if (FFlag::DebugLuauTimeTracing) { diff --git a/CodeGen/src/CodeGen.cpp b/CodeGen/src/CodeGen.cpp index bada61cd..bf500d69 100644 --- a/CodeGen/src/CodeGen.cpp +++ b/CodeGen/src/CodeGen.cpp @@ -57,8 +57,6 @@ LUAU_FASTINTVARIABLE(CodegenHeuristicsBlockLimit, 32'768) // 32 K // Current value is based on some member variables being limited to 16 bits LUAU_FASTINTVARIABLE(CodegenHeuristicsBlockInstructionLimit, 65'536) // 64 K -LUAU_FASTFLAGVARIABLE(DisableNativeCodegenIfBreakpointIsSet, false) - namespace Luau { namespace CodeGen @@ -302,7 +300,7 @@ void create(lua_State* L, AllocationCallback* allocationCallback, void* allocati ecb->close = onCloseState; ecb->destroy = onDestroyFunction; ecb->enter = onEnter; - ecb->disable = FFlag::DisableNativeCodegenIfBreakpointIsSet ? onDisable : nullptr; + ecb->disable = onDisable; } void create(lua_State* L) diff --git a/CodeGen/src/IrValueLocationTracking.cpp b/CodeGen/src/IrValueLocationTracking.cpp index 70fd7fc4..182dd736 100644 --- a/CodeGen/src/IrValueLocationTracking.cpp +++ b/CodeGen/src/IrValueLocationTracking.cpp @@ -3,6 +3,8 @@ #include "Luau/IrUtils.h" +LUAU_DYNAMIC_FASTFLAGVARIABLE(LuauCodegenTrackingMultilocationFix, false) + namespace Luau { namespace CodeGen @@ -159,6 +161,9 @@ void IrValueLocationTracking::afterInstLowering(IrInst& inst, uint32_t instIdx) case IrCmd::LOAD_DOUBLE: case IrCmd::LOAD_INT: case IrCmd::LOAD_TVALUE: + if (DFFlag::LuauCodegenTrackingMultilocationFix && inst.a.kind == IrOpKind::VmReg) + invalidateRestoreOp(inst.a, /*skipValueInvalidation*/ false); + recordRestoreOp(instIdx, inst.a); break; case IrCmd::STORE_POINTER: diff --git a/Sources.cmake b/Sources.cmake index 1f9aa5a7..5745f6dd 100644 --- a/Sources.cmake +++ b/Sources.cmake @@ -176,6 +176,7 @@ target_sources(Luau.Analysis PRIVATE Analysis/include/Luau/GlobalTypes.h Analysis/include/Luau/InsertionOrderedMap.h Analysis/include/Luau/Instantiation.h + Analysis/include/Luau/Instantiation2.h Analysis/include/Luau/IostreamHelpers.h Analysis/include/Luau/JsonEmitter.h Analysis/include/Luau/Linter.h @@ -242,6 +243,7 @@ target_sources(Luau.Analysis PRIVATE Analysis/src/Frontend.cpp Analysis/src/GlobalTypes.cpp Analysis/src/Instantiation.cpp + Analysis/src/Instantiation2.cpp Analysis/src/IostreamHelpers.cpp Analysis/src/JsonEmitter.cpp Analysis/src/Linter.cpp @@ -457,7 +459,6 @@ if(TARGET Luau.UnitTest) tests/TypeInfer.primitives.test.cpp tests/TypeInfer.provisional.test.cpp tests/TypeInfer.refinements.test.cpp - tests/TypeInfer.rwprops.test.cpp tests/TypeInfer.singletons.test.cpp tests/TypeInfer.tables.test.cpp tests/TypeInfer.test.cpp diff --git a/tests/Conformance.test.cpp b/tests/Conformance.test.cpp index 3293b246..4ce13658 100644 --- a/tests/Conformance.test.cpp +++ b/tests/Conformance.test.cpp @@ -31,6 +31,7 @@ LUAU_FASTFLAG(LuauSciNumberSkipTrailDot) LUAU_DYNAMIC_FASTFLAG(LuauInterruptablePatternMatch) LUAU_FASTINT(CodegenHeuristicsInstructionLimit) LUAU_DYNAMIC_FASTFLAG(LuauCodeGenFixBufferLenCheckA64) +LUAU_DYNAMIC_FASTFLAG(LuauCodegenTrackingMultilocationFix) static lua_CompileOptions defaultOptions() { @@ -2040,6 +2041,7 @@ TEST_CASE("SafeEnv") TEST_CASE("Native") { ScopedFastFlag luauCodeGenFixBufferLenCheckA64{DFFlag::LuauCodeGenFixBufferLenCheckA64, true}; + ScopedFastFlag luauCodegenTrackingMultilocationFix{DFFlag::LuauCodegenTrackingMultilocationFix, true}; // This tests requires code to run natively, otherwise all 'is_native' checks will fail if (!codegen || !luau_codegen_supported()) diff --git a/tests/ConstraintSolver.test.cpp b/tests/ConstraintSolver.test.cpp index 204d4d14..170510bd 100644 --- a/tests/ConstraintSolver.test.cpp +++ b/tests/ConstraintSolver.test.cpp @@ -17,7 +17,7 @@ static TypeId requireBinding(Scope* scope, const char* name) TEST_SUITE_BEGIN("ConstraintSolver"); -TEST_CASE_FIXTURE(ConstraintGeneratorFixture, "hello") +TEST_CASE_FIXTURE(ConstraintGeneratorFixture, "constraint_basics") { solve(R"( local a = 55 @@ -58,12 +58,7 @@ TEST_CASE_FIXTURE(ConstraintGeneratorFixture, "proper_let_generalization") TypeId idType = requireBinding(rootScope, "b"); - ToStringOptions opts; - - if (FFlag::DebugLuauDeferredConstraintResolution) - CHECK("(unknown) -> number" == toString(idType, opts)); - else - CHECK("(a) -> number" == toString(idType, opts)); + CHECK("(unknown) -> number" == toString(idType)); } TEST_SUITE_END(); diff --git a/tests/Differ.test.cpp b/tests/Differ.test.cpp index 9cf8c3aa..b009cec0 100644 --- a/tests/Differ.test.cpp +++ b/tests/Differ.test.cpp @@ -204,6 +204,8 @@ TEST_CASE_FIXTURE(DifferFixture, "right_cyclic_table_left_table_property_wrong") TEST_CASE_FIXTURE(DifferFixture, "equal_table_two_cyclic_tables_are_not_different") { + ScopedFastFlag sff{FFlag::DebugLuauDeferredConstraintResolution, false}; + CheckResult result = check(R"( local function id(x: a): a return x diff --git a/tests/Linter.test.cpp b/tests/Linter.test.cpp index b0e01dce..9f7aa77a 100644 --- a/tests/Linter.test.cpp +++ b/tests/Linter.test.cpp @@ -7,6 +7,8 @@ #include "doctest.h" +LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution); + using namespace Luau; TEST_SUITE_BEGIN("Linter"); @@ -1246,6 +1248,30 @@ _ = { CHECK_EQ(result.warnings[5].text, "Table index 1 is a duplicate; previously defined at line 36"); } +TEST_CASE_FIXTURE(Fixture, "read_write_table_props") +{ + ScopedFastFlag sff{FFlag::DebugLuauDeferredConstraintResolution, true}; + + LintResult result = lint(R"(-- line 1 + type A = {x: number} + type B = {read x: number, write x: number} + type C = {x: number, read x: number} -- line 4 + type D = {x: number, write x: number} + type E = {read x: number, x: boolean} + type F = {read x: number, read x: number} + type G = {write x: number, x: boolean} + type H = {write x: number, write x: boolean} + )"); + + REQUIRE(6 == result.warnings.size()); + CHECK(result.warnings[0].text == "Table type field 'x' is already read-write; previously defined at line 4"); + CHECK(result.warnings[1].text == "Table type field 'x' is already read-write; previously defined at line 5"); + CHECK(result.warnings[2].text == "Table type field 'x' already has a read type defined at line 6"); + CHECK(result.warnings[3].text == "Table type field 'x' is a duplicate; previously defined at line 7"); + CHECK(result.warnings[4].text == "Table type field 'x' already has a write type defined at line 8"); + CHECK(result.warnings[5].text == "Table type field 'x' is a duplicate; previously defined at line 9"); +} + TEST_CASE_FIXTURE(Fixture, "ImportOnlyUsedInTypeAnnotation") { LintResult result = lint(R"( diff --git a/tests/Normalize.test.cpp b/tests/Normalize.test.cpp index 2b09603e..81cb7cee 100644 --- a/tests/Normalize.test.cpp +++ b/tests/Normalize.test.cpp @@ -940,4 +940,26 @@ TEST_CASE_FIXTURE(NormalizeFixture, "normalize_unknown") CHECK(toString(normalizer.typeFromNormal(*nt)) == "unknown"); } +TEST_CASE_FIXTURE(NormalizeFixture, "read_only_props") +{ + ScopedFastFlag sff{FFlag::DebugLuauDeferredConstraintResolution, true}; + + CHECK("{ x: string }" == toString(normal("{ read x: string } & { x: string }"), {true})); + CHECK("{ x: string }" == toString(normal("{ x: string } & { read x: string }"), {true})); +} + +TEST_CASE_FIXTURE(NormalizeFixture, "read_only_props_2") +{ + ScopedFastFlag sff{FFlag::DebugLuauDeferredConstraintResolution, true}; + + CHECK(R"({ x: never })" == toString(normal(R"({ x: "hello" } & { x: "world" })"), {true})); +} + +TEST_CASE_FIXTURE(NormalizeFixture, "read_only_props_3") +{ + ScopedFastFlag sff{FFlag::DebugLuauDeferredConstraintResolution, true}; + + CHECK("{ read x: never }" == toString(normal(R"({ read x: "hello" } & { read x: "world" })"), {true})); +} + TEST_SUITE_END(); diff --git a/tests/Subtyping.test.cpp b/tests/Subtyping.test.cpp index 80a4c058..d42856c2 100644 --- a/tests/Subtyping.test.cpp +++ b/tests/Subtyping.test.cpp @@ -15,6 +15,8 @@ #include +LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution); + using namespace Luau; namespace Luau @@ -65,6 +67,8 @@ struct SubtypeFixture : Fixture UnifierSharedState sharedState{&ice}; Normalizer normalizer{&arena, builtinTypes, NotNull{&sharedState}}; + ScopedFastFlag sff{FFlag::DebugLuauDeferredConstraintResolution, true}; + ScopePtr rootScope{new Scope(builtinTypes->emptyTypePack)}; ScopePtr moduleScope{new Scope(rootScope)}; @@ -220,6 +224,11 @@ struct SubtypeFixture : Fixture {"Y", builtinTypes->numberType}, }); + TypeId readOnlyVec2Class = cls("ReadOnlyVec2", { + {"X", Property::readonly(builtinTypes->numberType)}, + {"Y", Property::readonly(builtinTypes->numberType)}, + }); + // "hello" | "hello" TypeId helloOrHelloType = arena.addType(UnionType{{helloType, helloType}}); @@ -787,6 +796,34 @@ TEST_CASE_FIXTURE(SubtypeFixture, "{x: (T) -> ()} <: {x: (U) -> ()}") CHECK_IS_SUBTYPE(tbl({{"x", genericTToNothingType}}), tbl({{"x", genericUToNothingType}})); } +TEST_CASE_FIXTURE(SubtypeFixture, "{ x: number } <: { read x: number }") +{ + ScopedFastFlag sff{FFlag::DebugLuauDeferredConstraintResolution, true}; + + CHECK_IS_SUBTYPE(tbl({{"x", builtinTypes->numberType}}), tbl({{"x", Property::readonly(builtinTypes->numberType)}})); +} + +TEST_CASE_FIXTURE(SubtypeFixture, "{ x: number } <: { write x: number }") +{ + ScopedFastFlag sff{FFlag::DebugLuauDeferredConstraintResolution, true}; + + CHECK_IS_SUBTYPE(tbl({{"x", builtinTypes->numberType}}), tbl({{"x", Property::writeonly(builtinTypes->numberType)}})); +} + +TEST_CASE_FIXTURE(SubtypeFixture, "{ x: \"hello\" } <: { read x: string }") +{ + ScopedFastFlag sff{FFlag::DebugLuauDeferredConstraintResolution, true}; + + CHECK_IS_SUBTYPE(tbl({{"x", helloType}}), tbl({{"x", Property::readonly(builtinTypes->stringType)}})); +} + +TEST_CASE_FIXTURE(SubtypeFixture, "{ x: string } <: { write x: string }") +{ + ScopedFastFlag sff{FFlag::DebugLuauDeferredConstraintResolution, true}; + + CHECK_IS_SUBTYPE(tbl({{"x", builtinTypes->stringType}}), tbl({{"x", Property::writeonly(builtinTypes->stringType)}})); +} + TEST_CASE_FIXTURE(SubtypeFixture, "{ @metatable { x: number } } <: { @metatable {} }") { CHECK_IS_SUBTYPE(meta({{"x", builtinTypes->numberType}}), meta({})); @@ -1027,6 +1064,28 @@ TEST_CASE_FIXTURE(SubtypeFixture, "Vec2 tableType, xy)); } +TEST_CASE_FIXTURE(SubtypeFixture, "ReadOnlyVec2 numberType}, {"Y", builtinTypes->numberType}})); +} + +TEST_CASE_FIXTURE(SubtypeFixture, "ReadOnlyVec2 <: { read X: number, read Y: number}") +{ + CHECK_IS_SUBTYPE( + readOnlyVec2Class, tbl({{"X", Property::readonly(builtinTypes->numberType)}, {"Y", Property::readonly(builtinTypes->numberType)}})); +} + +TEST_IS_SUBTYPE(vec2Class, tbl({{"X", Property::readonly(builtinTypes->numberType)}, {"Y", Property::readonly(builtinTypes->numberType)}})); + +TEST_IS_NOT_SUBTYPE(tbl({{"P", grandchildOneClass}}), tbl({{"P", Property::rw(rootClass)}})); +TEST_IS_SUBTYPE(tbl({{"P", grandchildOneClass}}), tbl({{"P", Property::readonly(rootClass)}})); +TEST_IS_SUBTYPE(tbl({{"P", rootClass}}), tbl({{"P", Property::writeonly(grandchildOneClass)}})); + +TEST_IS_NOT_SUBTYPE(cls("HasChild", {{"P", childClass}}), tbl({{"P", rootClass}})); +TEST_IS_SUBTYPE(cls("HasChild", {{"P", childClass}}), tbl({{"P", Property::readonly(rootClass)}})); +TEST_IS_NOT_SUBTYPE(cls("HasChild", {{"P", childClass}}), tbl({{"P", grandchildOneClass}})); +TEST_IS_SUBTYPE(cls("HasChild", {{"P", childClass}}), tbl({{"P", Property::writeonly(grandchildOneClass)}})); + TEST_CASE_FIXTURE(SubtypeFixture, "\"hello\" <: { lower : (string) -> string }") { CHECK_IS_SUBTYPE(helloType, tableWithLower); @@ -1217,8 +1276,8 @@ 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")), - /* superPath */ Path(TypePath::Property("X")), + CHECK(result.reasoning == std::vector{SubtypingReasoning{/* subPath */ Path(TypePath::Property::read("X")), + /* superPath */ Path(TypePath::Property::read("X")), /* variance */ SubtypingVariance::Invariant}}); } @@ -1317,8 +1376,8 @@ TEST_CASE_FIXTURE(SubtypeFixture, "nested_table_properties") SubtypingResult result = isSubtype(subTy, superTy); CHECK(!result.isSubtype); 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(), + /* subPath */ TypePath::PathBuilder().readProp("X").readProp("Y").readProp("Z").build(), + /* superPath */ TypePath::PathBuilder().readProp("X").readProp("Y").readProp("Z").build(), /* variance */ SubtypingVariance::Invariant, }}); } @@ -1335,7 +1394,7 @@ TEST_CASE_FIXTURE(SubtypeFixture, "string_table_mt") // metatable is empty, and abort there, without looking at the metatable // properties (because there aren't any). CHECK(result.reasoning == std::vector{SubtypingReasoning{ - /* subPath */ TypePath::PathBuilder().mt().prop("__index").build(), + /* subPath */ TypePath::PathBuilder().mt().readProp("__index").build(), /* superPath */ TypePath::kEmpty, }}); } @@ -1360,12 +1419,13 @@ 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")), - /* variance */ SubtypingVariance::Invariant}, - SubtypingReasoning{/* subPath */ Path(TypePath::Property("Y")), /* superPath */ Path(TypePath::Property("Y")), - /* variance */ SubtypingVariance::Invariant}, - }); + CHECK(result.reasoning == + std::vector{ + SubtypingReasoning{/* subPath */ Path(TypePath::Property::read("X")), /* superPath */ Path(TypePath::Property::read("X")), + /* variance */ SubtypingVariance::Invariant}, + SubtypingReasoning{/* subPath */ Path(TypePath::Property::read("Y")), /* superPath */ Path(TypePath::Property::read("Y")), + /* variance */ SubtypingVariance::Invariant}, + }); } TEST_SUITE_END(); diff --git a/tests/ToString.test.cpp b/tests/ToString.test.cpp index c2e4aadd..b9c0381f 100644 --- a/tests/ToString.test.cpp +++ b/tests/ToString.test.cpp @@ -931,18 +931,17 @@ TEST_CASE_FIXTURE(Fixture, "tostring_unsee_ttv_if_array") TEST_CASE_FIXTURE(Fixture, "tostring_error_mismatch") { CheckResult result = check(R"( ---!strict - function f1() : {a : number, b : string, c : { d : number}} - return { a = 1, b = "a", c = {d = "a"}} - end + --!strict + function f1() : {a : number, b : string, c : { d : number}} + return { a = 1, b = "a", c = {d = "a"}} + end + )"); -)"); - //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 exactly number)" - : - R"(Type + std::string expected; + if (FFlag::DebugLuauDeferredConstraintResolution) + expected = R"(Type pack '{ a: number, b: string, c: { d: string } }' could not be converted into '{ a: number, b: string, c: { d: number } }'; at [0][read "c"][read "d"], string is not exactly number)"; + else + expected = R"(Type '{ a: number, b: string, c: { d: string } }' could not be converted into '{| a: number, b: string, c: {| d: number |} |}' @@ -955,7 +954,6 @@ could not be converted into caused by: Property 'd' is not compatible. Type 'string' could not be converted into 'number' in an invariant context)"; - //clang-format on LUAU_REQUIRE_ERROR_COUNT(1, result); @@ -984,4 +982,20 @@ local f = abs TypeId fn = requireType("f"); CHECK("@checked (number) -> number" == toString(fn)); } + +TEST_CASE_FIXTURE(Fixture, "read_only_properties") +{ + ScopedFastFlag sff{FFlag::DebugLuauDeferredConstraintResolution, true}; + + CheckResult result = check(R"( + type A = {x: string} + type B = {read x: string} + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + + CHECK("{ x: string }" == toString(requireTypeAlias("A"), {true})); + CHECK("{ read x: string }" == toString(requireTypeAlias("B"), {true})); +} + TEST_SUITE_END(); diff --git a/tests/TypeInfer.aliases.test.cpp b/tests/TypeInfer.aliases.test.cpp index a9f358eb..52930087 100644 --- a/tests/TypeInfer.aliases.test.cpp +++ b/tests/TypeInfer.aliases.test.cpp @@ -202,7 +202,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 exactly number)"; + const std::string expected = R"(Type 'bad' could not be converted into 'T'; at [read "v"], string is not exactly number)"; CHECK(result.errors[0].location == Location{{4, 31}, {4, 44}}); CHECK_EQ(expected, toString(result.errors[0])); } @@ -221,7 +221,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 exactly number)"; + const std::string expected = R"(Type 'bad' could not be converted into 'U'; at [read "t"][read "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.builtins.test.cpp b/tests/TypeInfer.builtins.test.cpp index 45f5d01c..11bb5eda 100644 --- a/tests/TypeInfer.builtins.test.cpp +++ b/tests/TypeInfer.builtins.test.cpp @@ -386,10 +386,7 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "setmetatable_on_union_of_tables") LUAU_REQUIRE_NO_ERRORS(result); - if (FFlag::DebugLuauDeferredConstraintResolution) - CHECK("{ @metatable {| |}, A } | { @metatable {| |}, B }" == toString(requireTypeAlias("X"))); - else - CHECK("{ @metatable { }, A } | { @metatable { }, B }" == toString(requireTypeAlias("X"))); + CHECK("{ @metatable { }, A } | { @metatable { }, B }" == toString(requireTypeAlias("X"))); } TEST_CASE_FIXTURE(BuiltinsFixture, "table_insert_correctly_infers_type_of_array_2_args_overload") @@ -1012,6 +1009,22 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "assert_removes_falsy_types2") CHECK_EQ("((boolean | number)?) -> number | true", toString(requireType("f"))); } +TEST_CASE_FIXTURE(BuiltinsFixture, "assert_removes_falsy_types3") +{ + CheckResult result = check(R"( + local function f(x: (number | boolean)?) + assert(x) + return x + end + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + if (FFlag::DebugLuauDeferredConstraintResolution) + CHECK_EQ("((boolean | number)?) -> number | true", toString(requireType("f"))); + else // without the annotation, the old solver doesn't infer the best return type here + CHECK_EQ("((boolean | number)?) -> boolean | number", toString(requireType("f"))); +} + TEST_CASE_FIXTURE(BuiltinsFixture, "assert_removes_falsy_types_even_from_type_pack_tail_but_only_for_the_first_type") { CheckResult result = check(R"( diff --git a/tests/TypeInfer.classes.test.cpp b/tests/TypeInfer.classes.test.cpp index 00023b42..0fdea695 100644 --- a/tests/TypeInfer.classes.test.cpp +++ b/tests/TypeInfer.classes.test.cpp @@ -463,7 +463,7 @@ local b: B = a LUAU_REQUIRE_ERRORS(result); if (FFlag::DebugLuauDeferredConstraintResolution) - CHECK(toString(result.errors.at(0)) == "Type 'a' could not be converted into 'B'; at [\"x\"], ChildClass is not exactly BaseClass"); + CHECK(toString(result.errors.at(0)) == "Type 'a' could not be converted into 'B'; at [read \"x\"], ChildClass is not exactly BaseClass"); else { const std::string expected = R"(Type 'A' could not be converted into 'B' @@ -639,4 +639,58 @@ TEST_CASE_FIXTURE(ClassFixture, "indexable_classes") } } +TEST_CASE_FIXTURE(Fixture, "read_write_class_properties") +{ + ScopedFastFlag sff{FFlag::DebugLuauDeferredConstraintResolution, true}; + + TypeArena& arena = frontend.globals.globalTypes; + + unfreeze(arena); + + TypeId instanceType = arena.addType(ClassType{"Instance", {}, nullopt, nullopt, {}, {}, "Test"}); + getMutable(instanceType)->props = { + {"Parent", Property::rw(instanceType)} + }; + + // + + TypeId workspaceType = arena.addType(ClassType{"Workspace", {}, nullopt, nullopt, {}, {}, "Test"}); + + TypeId scriptType = arena.addType(ClassType{ + "Script", { + {"Parent", Property::rw(workspaceType, instanceType)} + }, + instanceType, nullopt, {}, {}, "Test" + }); + + TypeId partType = arena.addType(ClassType{ + "Part", { + {"BrickColor", Property::rw(builtinTypes->stringType)}, + {"Parent", Property::rw(workspaceType, instanceType)} + }, + instanceType, nullopt, {}, {}, "Test"}); + + getMutable(workspaceType)->props = { + {"Script", Property::readonly(scriptType)}, + {"Part", Property::readonly(partType)} + }; + + frontend.globals.globalScope->bindings[frontend.globals.globalNames.names->getOrAdd("script")] = Binding{scriptType}; + + freeze(arena); + + CheckResult result = check(R"( + script.Parent.Part.BrickColor = 0xFFFFFF + script.Parent.Part.Parent = script + )"); + + LUAU_REQUIRE_ERROR_COUNT(1, result); + + CHECK(Location{{1, 40}, {1, 48}} == result.errors[0].location); + TypeMismatch* tm = get(result.errors[0]); + REQUIRE(tm); + CHECK(builtinTypes->stringType == tm->wantedType); + CHECK(builtinTypes->numberType == tm->givenType); +} + TEST_SUITE_END(); diff --git a/tests/TypeInfer.generics.test.cpp b/tests/TypeInfer.generics.test.cpp index 8b647f99..a5f17f77 100644 --- a/tests/TypeInfer.generics.test.cpp +++ b/tests/TypeInfer.generics.test.cpp @@ -27,6 +27,8 @@ TEST_CASE_FIXTURE(Fixture, "check_generic_function") local y: number = id(37) )"); LUAU_REQUIRE_NO_ERRORS(result); + CHECK_EQ(builtinTypes->stringType, requireType("x")); + CHECK_EQ(builtinTypes->numberType, requireType("y")); } TEST_CASE_FIXTURE(Fixture, "check_generic_local_function") @@ -39,6 +41,40 @@ TEST_CASE_FIXTURE(Fixture, "check_generic_local_function") local y: number = id(37) )"); LUAU_REQUIRE_NO_ERRORS(result); + CHECK_EQ(builtinTypes->stringType, requireType("x")); + CHECK_EQ(builtinTypes->numberType, requireType("y")); +} + +TEST_CASE_FIXTURE(Fixture, "check_generic_local_function2") +{ + CheckResult result = check(R"( + local function id(x:a): a + return x + end + local x = id("hi") + local y = id(37) + )"); + LUAU_REQUIRE_NO_ERRORS(result); + CHECK_EQ(builtinTypes->stringType, requireType("x")); + CHECK_EQ(builtinTypes->numberType, requireType("y")); +} + +TEST_CASE_FIXTURE(BuiltinsFixture, "unions_and_generics") +{ + CheckResult result = check(R"( + type foo = (T | {T}) -> T + local foo = (nil :: any) :: foo + + type Test = number | {number} + local res = foo(1 :: Test) + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + + if (FFlag::DebugLuauDeferredConstraintResolution) + CHECK_EQ("number | {number}", toString(requireType("res"))); + else // in the old solver, this just totally falls apart + CHECK_EQ("a", toString(requireType("res"))); } TEST_CASE_FIXTURE(Fixture, "check_generic_typepack_function") @@ -370,7 +406,7 @@ TEST_CASE_FIXTURE(Fixture, "calling_self_generic_methods") { LUAU_REQUIRE_NO_ERRORS(result); - CHECK_EQ("{ f: (t1) -> (), id: (unknown, a) -> a } where t1 = { id: ((t1, number) -> number) & ((t1, string) -> string) }", + CHECK_EQ("{ f: (t1) -> (), id: (unknown, a) -> a } where t1 = { read id: ((t1, number) -> number) & ((t1, string) -> string) }", toString(requireType("x"), {true})); } else @@ -437,6 +473,7 @@ TEST_CASE_FIXTURE(Fixture, "dont_leak_generic_types") -- so this assignment should fail local b: boolean = f(true) )"); + LUAU_REQUIRE_ERRORS(result); } @@ -797,8 +834,9 @@ y.a.c = y LUAU_REQUIRE_ERRORS(result); 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))"); + CHECK( + toString(result.errors.at(0)) == + R"(Type 'x' could not be converted into 'T'; type x[read "a"][read "c"] (nil) is not exactly T[read "a"][read "c"][0] (T))"); else { const std::string expected = R"(Type 'y' could not be converted into 'T' @@ -1369,6 +1407,19 @@ TEST_CASE_FIXTURE(Fixture, "bidirectional_checking_and_generalization_play_nice" CHECK("string" == toString(requireType("b"))); } +TEST_CASE_FIXTURE(BuiltinsFixture, "generalization_no_cyclic_intersections") +{ + CheckResult result = check(R"( + local f, t, n = pairs({"foo"}) + local k, v = f(t) + )"); + + CHECK("({string}, number?) -> (number?, string)" == toString(requireType("f"))); + CHECK("{string}" == toString(requireType("t"))); + CHECK("number?" == toString(requireType("k"))); + CHECK("string" == toString(requireType("v"))); +} + TEST_CASE_FIXTURE(Fixture, "missing_generic_type_parameter") { CheckResult result = check(R"( @@ -1381,4 +1432,20 @@ TEST_CASE_FIXTURE(Fixture, "missing_generic_type_parameter") REQUIRE(get(result.errors[1])); } +TEST_CASE_FIXTURE(BuiltinsFixture, "generic_type_families_work_in_subtyping") +{ + if (!FFlag::DebugLuauDeferredConstraintResolution) + return; + + CheckResult result = check(R"( + local function addOne(x: T): add return x + 1 end + + local function six(): number + return addOne(5) + end + )"); + + LUAU_REQUIRE_NO_ERRORS(result); +} + TEST_SUITE_END(); diff --git a/tests/TypeInfer.modules.test.cpp b/tests/TypeInfer.modules.test.cpp index 13031560..97efef16 100644 --- a/tests/TypeInfer.modules.test.cpp +++ b/tests/TypeInfer.modules.test.cpp @@ -414,7 +414,7 @@ local b: B.T = a 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"); + CHECK(toString(result.errors.at(0)) == "Type 'a' could not be converted into 'T'; at [read \"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' @@ -455,7 +455,7 @@ local b: B.T = a 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"); + CHECK(toString(result.errors.at(0)) == "Type 'a' could not be converted into 'T'; at [read \"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' diff --git a/tests/TypeInfer.rwprops.test.cpp b/tests/TypeInfer.rwprops.test.cpp deleted file mode 100644 index b3ed7947..00000000 --- a/tests/TypeInfer.rwprops.test.cpp +++ /dev/null @@ -1,71 +0,0 @@ -// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details -#include "Fixture.h" - -#include "doctest.h" - -LUAU_FASTFLAG(DebugLuauReadWriteProperties) -LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution); - -using namespace Luau; - -namespace -{ - -struct ReadWriteFixture : Fixture -{ - ScopedFastFlag dcr{FFlag::DebugLuauDeferredConstraintResolution, true}; - - ReadWriteFixture() - : Fixture() - { - if (!FFlag::DebugLuauReadWriteProperties) - return; - - TypeArena* arena = &frontend.globals.globalTypes; - NotNull globalScope{frontend.globals.globalScope.get()}; - - unfreeze(*arena); - - TypeId genericT = arena->addType(GenericType{"T"}); - - TypeId readonlyX = arena->addType(TableType{TableState::Sealed, TypeLevel{}, globalScope}); - getMutable(readonlyX)->props["x"] = Property::readonly(genericT); - globalScope->addBuiltinTypeBinding("ReadonlyX", TypeFun{{{genericT}}, readonlyX}); - - freeze(*arena); - } -}; - -} // namespace - -TEST_SUITE_BEGIN("ReadWriteProperties"); - -TEST_CASE_FIXTURE(ReadWriteFixture, "read_from_a_readonly_prop") -{ - if (!FFlag::DebugLuauReadWriteProperties) - return; - - CheckResult result = check(R"( - function f(rx: ReadonlyX) - local x = rx.x - end - )"); - - LUAU_REQUIRE_NO_ERRORS(result); -} - -TEST_CASE_FIXTURE(ReadWriteFixture, "write_to_a_readonly_prop") -{ - if (!FFlag::DebugLuauReadWriteProperties) - return; - - CheckResult result = check(R"( - function f(rx: ReadonlyX) - rx.x = "hello!" -- error - end - )"); - - LUAU_REQUIRE_ERROR_COUNT(1, result); -} - -TEST_SUITE_END(); diff --git a/tests/TypeInfer.singletons.test.cpp b/tests/TypeInfer.singletons.test.cpp index 34fd902b..81cfabd9 100644 --- a/tests/TypeInfer.singletons.test.cpp +++ b/tests/TypeInfer.singletons.test.cpp @@ -360,7 +360,7 @@ TEST_CASE_FIXTURE(Fixture, "parametric_tagged_union_alias") type Result = Ok | Err local a : Result = {success = false, result = "hotdogs"} - local b : Result = {success = true, result = "hotdogs"} + -- local b : Result = {success = true, result = "hotdogs"} )"); LUAU_REQUIRE_ERROR_COUNT(1, result); diff --git a/tests/TypeInfer.tables.test.cpp b/tests/TypeInfer.tables.test.cpp index 05f6cb7e..a9ae8fc6 100644 --- a/tests/TypeInfer.tables.test.cpp +++ b/tests/TypeInfer.tables.test.cpp @@ -357,7 +357,7 @@ TEST_CASE_FIXTURE(Fixture, "open_table_unification_2") LUAU_REQUIRE_ERROR_COUNT(1, result); TypeError& err = result.errors[0]; MissingProperties* error = get(err); - REQUIRE(error != nullptr); + REQUIRE_MESSAGE(error != nullptr, "Expected MissingProperties but got " << toString(err)); REQUIRE(error->properties.size() == 1); CHECK_EQ("y", error->properties[0]); @@ -426,7 +426,7 @@ TEST_CASE_FIXTURE(Fixture, "table_param_width_subtyping_2") LUAU_REQUIRE_ERROR_COUNT(1, result); MissingProperties* error = get(result.errors[0]); - REQUIRE(error != nullptr); + REQUIRE_MESSAGE(error != nullptr, "Expected MissingProperties but got " << toString(result.errors[0])); REQUIRE(error->properties.size() == 1); CHECK_EQ("baz", error->properties[0]); @@ -446,7 +446,7 @@ TEST_CASE_FIXTURE(Fixture, "table_param_width_subtyping_3") LUAU_REQUIRE_ERROR_COUNT(1, result); TypeError& err = result.errors[0]; MissingProperties* error = get(err); - REQUIRE(error != nullptr); + REQUIRE_MESSAGE(error != nullptr, "Expected MissingProperties but got " << toString(err)); REQUIRE(error->properties.size() == 1); CHECK_EQ("baz", error->properties[0]); @@ -461,52 +461,6 @@ TEST_CASE_FIXTURE(Fixture, "table_param_width_subtyping_3") CHECK_EQ(err.location, (Location{Position{6, 8}, Position{6, 9}})); } -#if 0 -TEST_CASE_FIXTURE(Fixture, "table_param_width_subtyping_2") -{ - CheckResult result = check(R"( - function id(x) - return x - end - - function foo(o) - id(o.x) - id(o.y) - return o - end - - local a = {x=55, y=nil, w=3.14159} - local b = {} - b.x = 1 - b.y = 'hello' - b.z = 'something extra!' - - local q = foo(a) -- line 17 - local w = foo(b) -- line 18 - )"); - - LUAU_REQUIRE_NO_ERRORS(result); - - TypeId qType = requireType("q"); - const TableType* qTable = get(qType); - REQUIRE(qType != nullptr); - - CHECK(qTable->props.find("x") != qTable->props.end()); - CHECK(qTable->props.find("y") != qTable->props.end()); - CHECK(qTable->props.find("z") == qTable->props.end()); - CHECK(qTable->props.find("w") != qTable->props.end()); - - TypeId wType = requireType("w"); - const TableType* wTable = get(wType); - REQUIRE(wTable != nullptr); - - CHECK(wTable->props.find("x") != wTable->props.end()); - CHECK(wTable->props.find("y") != wTable->props.end()); - CHECK(wTable->props.find("z") != wTable->props.end()); - CHECK(wTable->props.find("w") == wTable->props.end()); -} -#endif - TEST_CASE_FIXTURE(Fixture, "table_unification_4") { CheckResult result = check(R"( @@ -680,7 +634,8 @@ TEST_CASE_FIXTURE(Fixture, "indexers_get_quantified_too") REQUIRE("number" == toString(indexer.indexType)); - REQUIRE(nullptr != get(follow(indexer.indexResultType))); + TypeId indexResultType = follow(indexer.indexResultType); + REQUIRE_MESSAGE(get(indexResultType), "Expected generic but got " << toString(indexResultType)); } TEST_CASE_FIXTURE(Fixture, "indexers_quantification_2") @@ -1077,6 +1032,8 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "meta_add_inferred") TEST_CASE_FIXTURE(BuiltinsFixture, "meta_add_both_ways") { + ScopedFastFlag sff{FFlag::DebugLuauDeferredConstraintResolution, false}; + CheckResult result = check(R"( type VectorMt = { __add: (Vector, number) -> Vector } local vectorMt: VectorMt @@ -1093,6 +1050,30 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "meta_add_both_ways") CHECK_EQ(*requireType("a"), *requireType("c")); } +TEST_CASE_FIXTURE(BuiltinsFixture, "meta_add_both_ways_lti") +{ + ScopedFastFlag sff{FFlag::DebugLuauDeferredConstraintResolution, true}; + + CheckResult result = check(R"( + local vectorMt = {} + + function vectorMt.__add(self: Vector, other: number) + return self + end + + type Vector = typeof(setmetatable({}, vectorMt)) + local a: Vector = setmetatable({}, vectorMt) + + local b = a + 2 + local c = 2 + a + )"); + LUAU_REQUIRE_NO_ERRORS(result); + + CHECK_EQ("Vector", toString(requireType("a"))); + CHECK_EQ(*requireType("a"), *requireType("b")); + CHECK_EQ(*requireType("a"), *requireType("c")); +} + // This test exposed a bug where we let go of the "seen" stack while unifying table types // As a result, type inference crashed with a stack overflow. TEST_CASE_FIXTURE(BuiltinsFixture, "unification_of_unions_in_a_self_referential_type") @@ -1570,7 +1551,7 @@ TEST_CASE_FIXTURE(Fixture, "right_table_missing_key2") LUAU_REQUIRE_ERROR_COUNT(1, result); MissingProperties* mp = get(result.errors[0]); - REQUIRE(mp); + REQUIRE_MESSAGE(mp, "Expected MissingProperties but got " << toString(result.errors[0])); CHECK_EQ(mp->context, MissingProperties::Missing); REQUIRE_EQ(1, mp->properties.size()); CHECK_EQ(mp->properties[0], "a"); @@ -1664,7 +1645,7 @@ TEST_CASE_FIXTURE(Fixture, "casting_tables_with_props_into_table_with_indexer4") if (FFlag::DebugLuauDeferredConstraintResolution) { LUAU_REQUIRE_ERROR_COUNT(1, result); - CHECK(toString(result.errors[0]) == "Type 'string' could not be converted into 'number'"); + CHECK(toString(result.errors[0]) == "Type 'number' could not be converted into 'string' in an invariant context"); } else { @@ -1685,7 +1666,7 @@ TEST_CASE_FIXTURE(Fixture, "table_subtyping_with_missing_props_dont_report_multi if (FFlag::DebugLuauDeferredConstraintResolution) { - CHECK_EQ("Type pack '{ x: number }' could not be converted into '{ x: number, y: number, z: number }'" + CHECK_EQ("Type pack '{ x: number }' could not be converted into '{ x: number, y: number, z: number }';" " at [0], { x: number } is not a subtype of { x: number, y: number, z: number }", toString(result.errors[0])); } @@ -2176,7 +2157,7 @@ local b: B = a LUAU_REQUIRE_ERRORS(result); 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)"); + CHECK(toString(result.errors.at(0)) == R"(Type 'a' could not be converted into 'B'; at [read "y"], number is not exactly string)"); else { const std::string expected = R"(Type 'A' could not be converted into 'B' @@ -2203,7 +2184,7 @@ local b: B = a LUAU_REQUIRE_ERRORS(result); 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)"); + CHECK(toString(result.errors.at(0)) == R"(Type 'a' could not be converted into 'B'; at [read "b"][read "y"], number is not exactly string)"); else { const std::string expected = R"(Type 'A' could not be converted into 'B' @@ -3979,15 +3960,18 @@ 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 exactly number" - "\n\tat [\"b\"], boolean is not exactly string" - "\n\tat [\"c\"], number is not exactly boolean"; + std::string expected = "Type 'a' could not be converted into 'T'; at [read \"a\"], string is not exactly number" + "\n\tat [read \"b\"], boolean is not exactly string" + "\n\tat [read \"c\"], number is not exactly boolean"; CHECK(toString(result.errors[0]) == expected); } TEST_CASE_FIXTURE(Fixture, "read_and_write_only_table_properties_are_unsupported") { - ScopedFastFlag sff{FFlag::LuauReadWritePropertySyntax, true}; + ScopedFastFlag sff[] = { + {FFlag::LuauReadWritePropertySyntax, true}, + {FFlag::DebugLuauDeferredConstraintResolution, false}, + }; CheckResult result = check(R"( type W = {read x: number} @@ -4026,6 +4010,22 @@ TEST_CASE_FIXTURE(Fixture, "read_ond_write_only_indexers_are_unsupported") CHECK(Location{{2, 18}, {2, 23}} == result.errors[1].location); } +TEST_CASE_FIXTURE(Fixture, "infer_write_property") +{ + ScopedFastFlag sff{FFlag::DebugLuauDeferredConstraintResolution, true}; + + CheckResult result = check(R"( + function f(t) + t.y = 1 + end + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + + // CHECK("({ y: number }) -> ()" == toString(requireType("f"))); + CHECK("({ y: number & unknown }) -> ()" == toString(requireType("f"))); +} + TEST_CASE_FIXTURE(Fixture, "table_subtyping_error_suppression") { CheckResult result = check(R"( @@ -4057,4 +4057,132 @@ TEST_CASE_FIXTURE(Fixture, "table_subtyping_error_suppression") } } +TEST_CASE_FIXTURE(Fixture, "write_to_read_only_property") +{ + ScopedFastFlag sff{FFlag::DebugLuauDeferredConstraintResolution, true}; + + CheckResult result = check(R"( + function f(t: {read x: number}) + t.x = 5 + end + )"); + + LUAU_REQUIRE_ERROR_COUNT(1, result); + + CHECK("Property x of table '{ read x: number }' is read-only" == toString(result.errors[0])); + + PropertyAccessViolation* pav = get(result.errors[0]); + REQUIRE(pav); + + CHECK("{ read x: number }" == toString(pav->table, {true})); + CHECK("x" == pav->key); + CHECK(PropertyAccessViolation::CannotWrite == pav->context); +} + +TEST_CASE_FIXTURE(Fixture, "write_to_unusually_named_read_only_property") +{ + ScopedFastFlag sff{FFlag::DebugLuauDeferredConstraintResolution, true}; + + CheckResult result = check(R"( + function f(t: {read ["hello world"]: number}) + t["hello world"] = 5 + end + )"); + + LUAU_REQUIRE_ERROR_COUNT(1, result); + + CHECK("Property \"hello world\" of table '{ read [\"hello world\"]: number }' is read-only" == toString(result.errors[0])); +} + +TEST_CASE_FIXTURE(Fixture, "write_annotations_are_unsupported_even_with_the_new_solver") +{ + ScopedFastFlag sff{FFlag::DebugLuauDeferredConstraintResolution, true}; + + CheckResult result = check(R"( + function f(t: {write foo: number}) + end + )"); + + LUAU_REQUIRE_ERROR_COUNT(1, result); + + CHECK("write keyword is illegal here" == toString(result.errors[0])); + CHECK(Location{{1, 23}, {1, 28}} == result.errors[0].location); +} + +TEST_CASE_FIXTURE(Fixture, "read_and_write_only_table_properties_are_unsupported") +{ + ScopedFastFlag sff[] = { + {FFlag::LuauReadWritePropertySyntax, true}, + {FFlag::DebugLuauDeferredConstraintResolution, false} + }; + + CheckResult result = check(R"( + type W = {read x: number} + type X = {write x: boolean} + + type Y = {read ["prop"]: boolean} + type Z = {write ["prop"]: string} + )"); + + LUAU_REQUIRE_ERROR_COUNT(4, result); + + CHECK("read keyword is illegal here" == toString(result.errors[0])); + CHECK(Location{{1, 18}, {1, 22}} == result.errors[0].location); + CHECK("write keyword is illegal here" == toString(result.errors[1])); + CHECK(Location{{2, 18}, {2, 23}} == result.errors[1].location); + CHECK("read keyword is illegal here" == toString(result.errors[2])); + CHECK(Location{{4, 18}, {4, 22}} == result.errors[2].location); + CHECK("write keyword is illegal here" == toString(result.errors[3])); + CHECK(Location{{5, 18}, {5, 23}} == result.errors[3].location); +} + +TEST_CASE_FIXTURE(Fixture, "read_ond_write_only_indexers_are_unsupported") +{ + ScopedFastFlag sff[] = { + {FFlag::LuauReadWritePropertySyntax, true}, + {FFlag::DebugLuauDeferredConstraintResolution, false} + }; + + CheckResult result = check(R"( + type T = {read [string]: number} + type U = {write [string]: boolean} + )"); + + LUAU_REQUIRE_ERROR_COUNT(2, result); + + CHECK("read keyword is illegal here" == toString(result.errors[0])); + CHECK(Location{{1, 18}, {1, 22}} == result.errors[0].location); + CHECK("write keyword is illegal here" == toString(result.errors[1])); + CHECK(Location{{2, 18}, {2, 23}} == result.errors[1].location); +} + +TEST_CASE_FIXTURE(Fixture, "table_writes_introduce_write_properties") +{ + ScopedFastFlag sff[] = { + {FFlag::LuauReadWritePropertySyntax, true}, + {FFlag::DebugLuauDeferredConstraintResolution, true} + }; + + CheckResult result = check(R"( + function oc(player, speaker) + local head = speaker.Character:FindFirstChild('Head') + speaker.Character = player[1].Character + end + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + + CHECK("({{ read Character: a }}, { Character: t1 }) -> () " + "where " + "t1 = a & { read FindFirstChild: (t1, string) -> (b, c...) }" == toString(requireType("oc"))); + +// We currently get +// ({{ read Character: a }}, { Character: t1 }) -> () where t1 = { read FindFirstChild: (t1, string) -> (b, c...) } + +// But we'd like to see +// ({{ read Character: t1 }}, { Character: t1 }) -> () where t1 = { read FindFirstChild: (t1, string) -> (a, b...) } + +// The type of speaker.Character should be the same as player[1].Character +} + TEST_SUITE_END(); diff --git a/tests/TypePath.test.cpp b/tests/TypePath.test.cpp index 697c2540..c5a7a2bb 100644 --- a/tests/TypePath.test.cpp +++ b/tests/TypePath.test.cpp @@ -15,8 +15,19 @@ using namespace Luau; using namespace Luau::TypePath; +LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution); LUAU_DYNAMIC_FASTINT(LuauTypePathMaximumTraverseSteps); +struct TypePathFixture : Fixture +{ + ScopedFastFlag sff1{FFlag::DebugLuauDeferredConstraintResolution, true}; +}; + +struct TypePathBuiltinsFixture : BuiltinsFixture +{ + ScopedFastFlag sff1{FFlag::DebugLuauDeferredConstraintResolution, true}; +}; + TEST_SUITE_BEGIN("TypePathManipulation"); TEST_CASE("append") @@ -95,12 +106,12 @@ TEST_SUITE_BEGIN("TypePathTraversal"); LUAU_REQUIRE_NO_ERRORS(result); \ } while (false); -TEST_CASE_FIXTURE(Fixture, "empty_traversal") +TEST_CASE_FIXTURE(TypePathFixture, "empty_traversal") { CHECK(traverseForType(builtinTypes->numberType, kEmpty, builtinTypes) == builtinTypes->numberType); } -TEST_CASE_FIXTURE(Fixture, "table_property") +TEST_CASE_FIXTURE(TypePathFixture, "table_property") { TYPESOLVE_CODE(R"( local x = { y = 123 } @@ -114,7 +125,7 @@ TEST_CASE_FIXTURE(ClassFixture, "class_property") CHECK(traverseForType(vector2InstanceType, Path(TypePath::Property{"X", true}), builtinTypes) == builtinTypes->numberType); } -TEST_CASE_FIXTURE(BuiltinsFixture, "metatable_property") +TEST_CASE_FIXTURE(TypePathBuiltinsFixture, "metatable_property") { SUBCASE("meta_does_not_contribute") { @@ -138,10 +149,10 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "metatable_property") )"); } - CHECK(traverseForType(requireType("x"), Path(TypePath::Property("x")), builtinTypes) == builtinTypes->numberType); + CHECK(traverseForType(requireType("x"), Path(TypePath::Property::read("x")), builtinTypes) == builtinTypes->numberType); } -TEST_CASE_FIXTURE(Fixture, "index") +TEST_CASE_FIXTURE(TypePathFixture, "index") { SUBCASE("unions") { @@ -242,7 +253,7 @@ TEST_CASE_FIXTURE(ClassFixture, "metatables") } } -TEST_CASE_FIXTURE(Fixture, "bounds") +TEST_CASE_FIXTURE(TypePathFixture, "bounds") { SUBCASE("free_type") { @@ -274,7 +285,7 @@ TEST_CASE_FIXTURE(Fixture, "bounds") } } -TEST_CASE_FIXTURE(Fixture, "indexers") +TEST_CASE_FIXTURE(TypePathFixture, "indexers") { SUBCASE("table") { @@ -308,7 +319,7 @@ TEST_CASE_FIXTURE(Fixture, "indexers") // TODO: Class types } -TEST_CASE_FIXTURE(Fixture, "negated") +TEST_CASE_FIXTURE(TypePathFixture, "negated") { SUBCASE("valid") { @@ -327,7 +338,7 @@ TEST_CASE_FIXTURE(Fixture, "negated") } } -TEST_CASE_FIXTURE(Fixture, "variadic") +TEST_CASE_FIXTURE(TypePathFixture, "variadic") { SUBCASE("valid") { @@ -346,7 +357,7 @@ TEST_CASE_FIXTURE(Fixture, "variadic") } } -TEST_CASE_FIXTURE(Fixture, "arguments") +TEST_CASE_FIXTURE(TypePathFixture, "arguments") { SUBCASE("function") { @@ -368,7 +379,7 @@ TEST_CASE_FIXTURE(Fixture, "arguments") } } -TEST_CASE_FIXTURE(Fixture, "returns") +TEST_CASE_FIXTURE(TypePathFixture, "returns") { SUBCASE("function") { @@ -391,7 +402,7 @@ TEST_CASE_FIXTURE(Fixture, "returns") } } -TEST_CASE_FIXTURE(Fixture, "tail") +TEST_CASE_FIXTURE(TypePathFixture, "tail") { SUBCASE("has_tail") { @@ -422,7 +433,7 @@ TEST_CASE_FIXTURE(Fixture, "tail") } } -TEST_CASE_FIXTURE(Fixture, "cycles" * doctest::timeout(0.5)) +TEST_CASE_FIXTURE(TypePathFixture, "cycles" * doctest::timeout(0.5)) { // This will fail an occurs check, but it's a quick example of a cyclic type // where there _is_ no traversal. @@ -451,7 +462,7 @@ TEST_CASE_FIXTURE(Fixture, "cycles" * doctest::timeout(0.5)) } } -TEST_CASE_FIXTURE(Fixture, "step_limit") +TEST_CASE_FIXTURE(TypePathFixture, "step_limit") { ScopedFastInt sfi(DFInt::LuauTypePathMaximumTraverseSteps, 2); @@ -466,12 +477,12 @@ TEST_CASE_FIXTURE(Fixture, "step_limit") )"); TypeId root = requireTypeAlias("T"); - Path path = PathBuilder().prop("x").prop("y").prop("z").build(); + Path path = PathBuilder().readProp("x").readProp("y").readProp("z").build(); auto result = traverseForType(root, path, builtinTypes); CHECK(!result); } -TEST_CASE_FIXTURE(BuiltinsFixture, "complex_chains") +TEST_CASE_FIXTURE(TypePathBuiltinsFixture, "complex_chains") { SUBCASE("add_metamethod_return_type") { @@ -484,7 +495,7 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "complex_chains") )"); TypeId root = requireTypeAlias("Tab"); - Path path = PathBuilder().mt().prop("__add").rets().index(0).build(); + Path path = PathBuilder().mt().readProp("__add").rets().index(0).build(); auto result = traverseForType(root, path, builtinTypes); CHECK(result == builtinTypes->numberType); } @@ -498,7 +509,7 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "complex_chains") )"); TypeId root = requireTypeAlias("Obj"); - Path path = PathBuilder().prop("method").index(0).args().index(1).build(); + Path path = PathBuilder().readProp("method").index(0).args().index(1).build(); auto result = traverseForType(root, path, builtinTypes); CHECK(*result == builtinTypes->falseType); } @@ -510,6 +521,10 @@ TEST_SUITE_BEGIN("TypePathToString"); TEST_CASE("field") { + ScopedFastFlag sff[] = { + {FFlag::DebugLuauDeferredConstraintResolution, false}, + }; + CHECK(toString(PathBuilder().prop("foo").build()) == R"(["foo"])"); } @@ -535,10 +550,26 @@ TEST_CASE("empty_path") TEST_CASE("prop") { + ScopedFastFlag sff[] = { + {FFlag::DebugLuauDeferredConstraintResolution, false}, + }; + Path p = PathBuilder().prop("foo").build(); CHECK(p == Path(TypePath::Property{"foo"})); } +TEST_CASE_FIXTURE(TypePathFixture, "readProp") +{ + Path p = PathBuilder().readProp("foo").build(); + CHECK(p == Path(TypePath::Property::read("foo"))); +} + +TEST_CASE_FIXTURE(TypePathFixture, "writeProp") +{ + Path p = PathBuilder().writeProp("foo").build(); + CHECK(p == Path(TypePath::Property::write("foo"))); +} + TEST_CASE("index") { Path p = PathBuilder().index(0).build(); @@ -561,8 +592,10 @@ TEST_CASE("fields") TEST_CASE("chained") { - CHECK(PathBuilder().index(0).prop("foo").mt().prop("bar").args().index(1).build() == - Path({Index{0}, TypePath::Property{"foo"}, TypeField::Metatable, TypePath::Property{"bar"}, PackField::Arguments, Index{1}})); + ScopedFastFlag sff{FFlag::DebugLuauDeferredConstraintResolution, true}; + + CHECK(PathBuilder().index(0).readProp("foo").mt().readProp("bar").args().index(1).build() == + Path({Index{0}, TypePath::Property::read("foo"), TypeField::Metatable, TypePath::Property::read("bar"), PackField::Arguments, Index{1}})); } TEST_SUITE_END(); // TypePathBuilder diff --git a/tests/Unifier2.test.cpp b/tests/Unifier2.test.cpp index 56edb4a7..842f9e06 100644 --- a/tests/Unifier2.test.cpp +++ b/tests/Unifier2.test.cpp @@ -76,13 +76,13 @@ TEST_CASE_FIXTURE(Unifier2Fixture, "T <: U") CHECK(u2.unify(left, right)); - CHECK("t1 where t1 = ('a <: (t1 <: 'b))" == toString(left)); - CHECK("t1 where t1 = (('a <: t1) <: 'b)" == toString(right)); + CHECK("'a" == toString(left)); + CHECK("'a" == toString(right)); CHECK("never" == toString(freeLeft->lowerBound)); - CHECK("t1 where t1 = (('a <: t1) <: 'b)" == toString(freeLeft->upperBound)); + CHECK("unknown" == toString(freeLeft->upperBound)); - CHECK("t1 where t1 = ('a <: (t1 <: 'b))" == toString(freeRight->lowerBound)); + CHECK("never" == toString(freeRight->lowerBound)); CHECK("unknown" == toString(freeRight->upperBound)); } diff --git a/tests/conformance/native.lua b/tests/conformance/native.lua index 9df7a3bb..6ebe85d6 100644 --- a/tests/conformance/native.lua +++ b/tests/conformance/native.lua @@ -293,6 +293,24 @@ end assert(loopIteratorProtocol(0, table.create(100, 5)) == 5058) +function valueTrackingIssue1() + local b = buffer.create(1) + buffer.writeu8(b, 0, 0) + local v1 + + local function closure() + assert(type(b) == "buffer") -- b is the first upvalue + v1 = nil -- v1 is the second upvalue + + -- prevent inlining + for i = 1, 100 do print(`{b} is {b}`) end + end + + closure() +end + +valueTrackingIssue1() + local function vec3compsum(a: vector) return a.X + a.Y + a.Z end diff --git a/tools/faillist.txt b/tools/faillist.txt index dd51634b..89675715 100644 --- a/tools/faillist.txt +++ b/tools/faillist.txt @@ -1,4 +1,3 @@ -AnnotationTests.typeof_expr AstQuery.last_argument_function_call_type AutocompleteTest.anonymous_autofilled_generic_on_argument_type_pack_vararg AutocompleteTest.anonymous_autofilled_generic_type_pack_vararg @@ -13,7 +12,6 @@ BuiltinTests.assert_removes_falsy_types_even_from_type_pack_tail_but_only_for_th BuiltinTests.assert_returns_false_and_string_iff_it_knows_the_first_argument_cannot_be_truthy BuiltinTests.bad_select_should_not_crash BuiltinTests.coroutine_resume_anything_goes -BuiltinTests.debug_info_is_crazy BuiltinTests.global_singleton_types_are_sealed BuiltinTests.gmatch_capture_types BuiltinTests.gmatch_capture_types2 @@ -67,24 +65,12 @@ DefinitionTests.class_definition_string_props DefinitionTests.declaring_generic_functions DefinitionTests.definition_file_classes Differ.equal_generictp_cyclic -Differ.equal_table_A_B_C -Differ.equal_table_cyclic_diamonds_unraveled -Differ.equal_table_kind_A -Differ.equal_table_kind_B -Differ.equal_table_kind_C -Differ.equal_table_kind_D -Differ.equal_table_measuring_tapes -Differ.equal_table_two_shifted_circles_are_not_different Differ.generictp_normal Differ.generictp_normal_2 -Differ.left_cyclic_table_right_table_missing_property -Differ.left_cyclic_table_right_table_property_wrong Differ.metatable_metamissing_left Differ.metatable_metamissing_right Differ.metatable_metanormal Differ.negation -Differ.right_cyclic_table_left_table_property_wrong -Differ.table_left_circle_right_measuring_tape FrontendTest.accumulate_cached_errors_in_consistent_order FrontendTest.environments FrontendTest.imported_table_modification_2 @@ -94,47 +80,37 @@ FrontendTest.trace_requires_in_nonstrict_mode GenericsTests.apply_type_function_nested_generics1 GenericsTests.better_mismatch_error_messages GenericsTests.bound_tables_do_not_clone_original_fields -GenericsTests.check_generic_function -GenericsTests.check_generic_local_function -GenericsTests.check_mutual_generic_functions -GenericsTests.check_mutual_generic_functions_errors -GenericsTests.check_mutual_generic_functions_unannotated -GenericsTests.check_nested_generic_function -GenericsTests.check_recursive_generic_function GenericsTests.correctly_instantiate_polymorphic_member_functions GenericsTests.do_not_always_instantiate_generic_intersection_types +GenericsTests.do_not_infer_generic_functions +GenericsTests.dont_leak_generic_types +GenericsTests.dont_leak_inferred_generic_types GenericsTests.dont_substitute_bound_types GenericsTests.error_detailed_function_mismatch_generic_pack GenericsTests.error_detailed_function_mismatch_generic_types GenericsTests.factories_of_generics -GenericsTests.function_arguments_can_be_polytypes GenericsTests.generic_argument_count_too_few GenericsTests.generic_argument_count_too_many GenericsTests.generic_factories GenericsTests.generic_functions_dont_cache_type_parameters GenericsTests.generic_functions_in_types +GenericsTests.generic_type_families_work_in_subtyping GenericsTests.generic_type_pack_parentheses GenericsTests.generic_type_pack_unification1 GenericsTests.generic_type_pack_unification2 GenericsTests.generic_type_pack_unification3 GenericsTests.higher_rank_polymorphism_should_not_accept_instantiated_arguments GenericsTests.hof_subtype_instantiation_regression -GenericsTests.infer_generic_function +GenericsTests.infer_generic_function_function_argument GenericsTests.infer_generic_function_function_argument_2 GenericsTests.infer_generic_function_function_argument_3 GenericsTests.infer_generic_function_function_argument_overloaded GenericsTests.infer_generic_lib_function_function_argument -GenericsTests.infer_generic_local_function -GenericsTests.infer_generic_property -GenericsTests.infer_nested_generic_function -GenericsTests.inferred_local_vars_can_be_polytypes GenericsTests.instantiated_function_argument_names -GenericsTests.local_vars_can_be_polytypes +GenericsTests.mutable_state_polymorphism GenericsTests.no_stack_overflow_from_quantifying GenericsTests.properties_can_be_instantiated_polytypes -GenericsTests.properties_can_be_polytypes GenericsTests.quantify_functions_even_if_they_have_an_explicit_generic -GenericsTests.rank_N_types_via_typeof GenericsTests.self_recursive_instantiated_param GenericsTests.type_parameters_can_be_polytypes GenericsTests.typefuns_sharing_types @@ -164,7 +140,6 @@ IntersectionTypes.overloadeded_functions_with_weird_typepacks_3 IntersectionTypes.overloadeded_functions_with_weird_typepacks_4 IntersectionTypes.table_write_sealed_indirect IntersectionTypes.union_saturate_overloaded_functions -Linter.DeprecatedApiFenv Linter.FormatStringTyped Linter.TableOperationsIndexer ModuleTests.clone_self_property @@ -210,16 +185,12 @@ RefinementTest.discriminate_from_isa_of_x RefinementTest.discriminate_from_truthiness_of_x RefinementTest.discriminate_tag RefinementTest.discriminate_tag_with_implicit_else -RefinementTest.else_with_no_explicit_expression_should_also_refine_the_tagged_union RefinementTest.fail_to_refine_a_property_of_subscript_expression RefinementTest.falsiness_of_TruthyPredicate_narrows_into_nil -RefinementTest.function_call_with_colon_after_refining_not_to_be_nil RefinementTest.globals_can_be_narrowed_too 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.narrow_property_of_a_bounded_variable RefinementTest.nonoptional_type_can_narrow_to_nil_if_sense_is_true RefinementTest.not_t_or_some_prop_of_t @@ -245,7 +216,6 @@ TableTests.any_when_indexing_into_an_unsealed_table_with_no_indexer_in_nonstrict TableTests.array_factory_function TableTests.casting_tables_with_props_into_table_with_indexer2 TableTests.casting_tables_with_props_into_table_with_indexer3 -TableTests.casting_tables_with_props_into_table_with_indexer4 TableTests.casting_unsealed_tables_with_props_into_table_with_indexer TableTests.checked_prop_too_early TableTests.cli_84607_missing_prop_in_array_or_dict @@ -258,12 +228,10 @@ TableTests.common_table_element_union_in_call TableTests.common_table_element_union_in_call_tail TableTests.common_table_element_union_in_prop TableTests.confusing_indexing -TableTests.cyclic_shifted_tables TableTests.disallow_indexing_into_an_unsealed_table_with_no_indexer_in_strict_mode TableTests.dont_crash_when_setmetatable_does_not_produce_a_metatabletypevar TableTests.dont_extend_unsealed_tables_in_rvalue_position TableTests.dont_leak_free_table_props -TableTests.dont_quantify_table_that_belongs_to_outer_scope TableTests.dont_suggest_exact_match_keys TableTests.error_detailed_indexer_key TableTests.error_detailed_indexer_value @@ -275,8 +243,8 @@ TableTests.generalize_table_argument TableTests.generic_table_instantiation_potential_regression TableTests.indexer_mismatch TableTests.indexers_get_quantified_too +TableTests.indexing_from_a_table_should_prefer_properties_when_possible TableTests.inequality_operators_imply_exactly_matching_types -TableTests.infer_indexer_from_array_like_table TableTests.infer_indexer_from_its_variable_type_and_unifiable TableTests.inferred_return_type_of_free_table TableTests.instantiate_table_cloning_3 @@ -286,7 +254,7 @@ TableTests.length_operator_intersection TableTests.length_operator_non_table_union TableTests.length_operator_union TableTests.less_exponential_blowup_please -TableTests.meta_add_both_ways +TableTests.meta_add TableTests.meta_add_inferred TableTests.metatable_mismatch_should_fail TableTests.missing_metatable_for_sealed_tables_do_not_get_inferred @@ -300,8 +268,6 @@ TableTests.oop_polymorphic TableTests.open_table_unification_2 TableTests.pass_a_union_of_tables_to_a_function_that_requires_a_table TableTests.pass_a_union_of_tables_to_a_function_that_requires_a_table_2 -TableTests.pass_incompatible_union_to_a_generic_table_without_crashing -TableTests.passing_compatible_unions_to_a_generic_table_without_crashing TableTests.persistent_sealed_table_is_immutable TableTests.prop_access_on_key_whose_types_mismatches TableTests.prop_access_on_unions_of_indexers_where_key_whose_types_mismatches @@ -320,12 +286,10 @@ TableTests.shared_selfs_from_free_param TableTests.shared_selfs_through_metatables TableTests.table_call_metamethod_basic TableTests.table_call_metamethod_must_be_callable -TableTests.table_function_check_use_after_free TableTests.table_param_width_subtyping_2 TableTests.table_param_width_subtyping_3 TableTests.table_simple_call TableTests.table_subtyping_with_extra_props_dont_report_multiple_errors -TableTests.table_subtyping_with_missing_props_dont_report_multiple_errors TableTests.table_subtyping_with_missing_props_dont_report_multiple_errors2 TableTests.table_unification_4 TableTests.table_unifies_into_map @@ -351,7 +315,6 @@ TryUnifyTests.members_of_failed_typepack_unification_are_unified_with_errorType TryUnifyTests.result_of_failed_typepack_unification_is_constrained TryUnifyTests.uninhabited_table_sub_anything TryUnifyTests.uninhabited_table_sub_never -TryUnifyTests.variadics_should_use_reversed_properly TypeAliases.dont_lose_track_of_PendingExpansionTypes_after_substitution TypeAliases.generic_param_remap TypeAliases.mismatched_generic_type_param @@ -376,10 +339,10 @@ TypeFamilyTests.internal_families_raise_errors TypeFamilyTests.table_internal_families TypeFamilyTests.type_families_inhabited_with_normalization TypeFamilyTests.unsolvable_family +TypeInfer.be_sure_to_use_active_txnlog_when_evaluating_a_variadic_overload TypeInfer.bidirectional_checking_of_callback_property TypeInfer.check_type_infer_recursion_count TypeInfer.checking_should_not_ice -TypeInfer.cli_39932_use_unifier_in_ensure_methods TypeInfer.cli_50041_committing_txnlog_in_apollo_client_error TypeInfer.dont_ice_when_failing_the_occurs_check TypeInfer.dont_report_type_errors_within_an_AstExprError @@ -447,7 +410,6 @@ TypeInferFunctions.function_exprs_are_generalized_at_signature_scope_not_enclosi TypeInferFunctions.function_is_supertype_of_concrete_functions TypeInferFunctions.function_statement_sealed_table_assignment_through_indexer TypeInferFunctions.generic_packs_are_not_variadic -TypeInferFunctions.higher_order_function_2 TypeInferFunctions.higher_order_function_4 TypeInferFunctions.improved_function_arg_mismatch_error_nonstrict TypeInferFunctions.improved_function_arg_mismatch_errors @@ -469,7 +431,6 @@ TypeInferFunctions.other_things_are_not_related_to_function TypeInferFunctions.param_1_and_2_both_takes_the_same_generic_but_their_arguments_are_incompatible TypeInferFunctions.param_1_and_2_both_takes_the_same_generic_but_their_arguments_are_incompatible_2 TypeInferFunctions.record_matching_overload -TypeInferFunctions.regex_benchmark_string_format_minimization TypeInferFunctions.report_exiting_without_return_nonstrict TypeInferFunctions.return_type_by_overload TypeInferFunctions.too_few_arguments_variadic @@ -492,8 +453,6 @@ 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_an_iterator_of_type_any -TypeInferLoops.for_in_with_generic_next -TypeInferLoops.for_in_with_just_one_iterator_is_ok TypeInferLoops.for_loop TypeInferLoops.ipairs_produces_integral_indices TypeInferLoops.iterate_over_free_table @@ -511,7 +470,6 @@ TypeInferLoops.properly_infer_iteratee_is_a_free_table TypeInferLoops.repeat_loop TypeInferLoops.varlist_declared_by_for_in_loop_should_be_free TypeInferLoops.while_loop -TypeInferModules.bound_free_table_export_is_ok TypeInferModules.custom_require_global TypeInferModules.do_not_modify_imported_types TypeInferModules.do_not_modify_imported_types_5 @@ -521,7 +479,6 @@ TypeInferOOP.cycle_between_object_constructor_and_alias TypeInferOOP.dont_suggest_using_colon_rather_than_dot_if_another_overload_works TypeInferOOP.dont_suggest_using_colon_rather_than_dot_if_it_wont_help_2 TypeInferOOP.dont_suggest_using_colon_rather_than_dot_if_not_defined_with_colon -TypeInferOOP.inferred_methods_of_free_tables_have_the_same_level_as_the_enclosing_table TypeInferOOP.inferring_hundreds_of_self_calls_should_not_suffocate_memory TypeInferOOP.methods_are_topologically_sorted TypeInferOOP.object_constructor_can_refer_to_method_of_self @@ -547,7 +504,6 @@ TypeInferOperators.strict_binary_op_where_lhs_unknown TypeInferOperators.typecheck_overloaded_multiply_that_is_an_intersection TypeInferOperators.typecheck_overloaded_multiply_that_is_an_intersection_on_rhs TypeInferOperators.typecheck_unary_len_error -TypeInferOperators.typecheck_unary_minus TypeInferOperators.typecheck_unary_minus_error TypeInferOperators.UnknownGlobalCompoundAssign TypeInferPrimitives.CheckMethodsOfNumber @@ -564,7 +520,6 @@ TypePackTests.pack_tail_unification_check TypePackTests.type_alias_backwards_compatible TypePackTests.type_alias_default_type_errors TypePackTests.type_alias_type_packs_import -TypePackTests.type_packs_with_tails_in_vararg_adjustment TypePackTests.unify_variadic_tails_in_arguments TypeSingletons.enums_using_singletons_mismatch TypeSingletons.error_detailed_tagged_union_mismatch_bool diff --git a/tools/lldb_formatters.py b/tools/lldb_formatters.py index 30654af3..8238cc90 100644 --- a/tools/lldb_formatters.py +++ b/tools/lldb_formatters.py @@ -385,7 +385,7 @@ def luau_typepath_property_summary(valobj, internal_dict, options): read_write = False try: fflag_valobj = valobj.GetFrame().GetValueForVariablePath( - "FFlag::DebugLuauReadWriteProperties::value") + "FFlag::DebugLuauDeferredConstraintResolution::value") read_write = fflag_valobj.GetValue() == "true" except Exception as e: diff --git a/tools/test_dcr.py b/tools/test_dcr.py index 3598b02c..0a23587a 100644 --- a/tools/test_dcr.py +++ b/tools/test_dcr.py @@ -107,12 +107,6 @@ def main(): action="store_true", help="Write a new faillist.txt after running tests.", ) - parser.add_argument( - "--rwp", - dest="rwp", - action="store_true", - help="Run the tests with read-write properties enabled.", - ) parser.add_argument( "--ts", dest="suite", @@ -135,8 +129,6 @@ def main(): failList = loadFailList() flags = ["true", "DebugLuauDeferredConstraintResolution"] - if args.rwp: - flags.append("DebugLuauReadWriteProperties") commandLine = [args.path, "--reporters=xml", "--fflags=" + ",".join(flags)]