From 3b0e93bec9634f5e9a885e989c50bcca93c6d966 Mon Sep 17 00:00:00 2001 From: Vighnesh-V Date: Fri, 23 Feb 2024 12:08:34 -0800 Subject: [PATCH] Sync to upstream/release/614 (#1173) # What's changed? Add program argument passing to scripts run using the Luau REPL! You can now pass `--program-args` (or shorthand `-a`) to the REPL which will treat all remaining arguments as arguments to pass to executed scripts. These values can be accessed through variadic argument expansion. You can read these values like so: ``` local args = {...} -- gets you an array of all the arguments ``` For example if we run the following script like `luau test.lua -a test1 test2 test3`: ``` -- test.lua print(...) ``` you should get the output: ``` test1 test2 test3 ``` ### Native Code Generation * Improve A64 lowering for vector operations by using vector instructions * Fix lowering issue in IR value location tracking! - A developer reported a divergence between code run in the VM and Native Code Generation which we have now fixed ### New Type Solver * Apply substitution to type families, and emit new constraints to reduce those further * More progress on reducing comparison (`lt/le`)type families * Resolve two major sources of cyclic types in the new solver ### Miscellaneous * Turned internal compiler errors (ICE's) into warnings and errors ------- Co-authored-by: Aaron Weiss Co-authored-by: Alexander McCord Co-authored-by: Andy Friesen Co-authored-by: Aviral Goel Co-authored-by: Vyacheslav Egorov --------- Co-authored-by: Aaron Weiss Co-authored-by: Alexander McCord Co-authored-by: Andy Friesen Co-authored-by: Aviral Goel Co-authored-by: David Cope Co-authored-by: Lily Brown Co-authored-by: Vyacheslav Egorov --- Analysis/include/Luau/Constraint.h | 14 +- Analysis/include/Luau/ConstraintGenerator.h | 2 + Analysis/include/Luau/ConstraintSolver.h | 57 ++-- Analysis/include/Luau/Error.h | 44 ++- Analysis/include/Luau/Instantiation2.h | 70 +++++ Analysis/include/Luau/Subtyping.h | 15 +- Analysis/include/Luau/Type.h | 12 +- Analysis/include/Luau/TypeFamily.h | 7 +- Analysis/include/Luau/TypeUtils.h | 2 + Analysis/include/Luau/Unifier2.h | 5 + Analysis/include/Luau/VisitType.h | 21 +- Analysis/src/AstQuery.cpp | 10 +- Analysis/src/Autocomplete.cpp | 5 +- Analysis/src/BuiltinDefinitions.cpp | 20 ++ Analysis/src/Clone.cpp | 14 +- Analysis/src/ConstraintGenerator.cpp | 106 ++++--- Analysis/src/ConstraintSolver.cpp | 330 ++++++++++++-------- Analysis/src/Error.cpp | 46 +++ Analysis/src/Frontend.cpp | 21 +- Analysis/src/Instantiation.cpp | 34 +- Analysis/src/Instantiation2.cpp | 38 +++ Analysis/src/IostreamHelpers.cpp | 6 + Analysis/src/Linter.cpp | 45 +++ Analysis/src/Normalize.cpp | 70 ++++- Analysis/src/Simplify.cpp | 83 +++-- Analysis/src/Substitution.cpp | 12 +- Analysis/src/Subtyping.cpp | 182 +++++++++-- Analysis/src/ToString.cpp | 57 ++-- Analysis/src/Type.cpp | 34 +- Analysis/src/TypeChecker2.cpp | 74 +++-- Analysis/src/TypeFamily.cpp | 26 +- Analysis/src/TypePath.cpp | 18 +- Analysis/src/TypeUtils.cpp | 21 +- Analysis/src/Unifier2.cpp | 164 ++++++++-- Ast/src/Parser.cpp | 3 +- CLI/FileUtils.cpp | 4 + CLI/Reduce.cpp | 4 +- CLI/Repl.cpp | 24 +- CodeGen/src/CodeGen.cpp | 4 +- CodeGen/src/IrValueLocationTracking.cpp | 5 + Sources.cmake | 3 +- tests/Conformance.test.cpp | 2 + tests/ConstraintSolver.test.cpp | 9 +- tests/Differ.test.cpp | 2 + tests/Linter.test.cpp | 26 ++ tests/Normalize.test.cpp | 22 ++ tests/Subtyping.test.cpp | 82 ++++- tests/ToString.test.cpp | 38 ++- tests/TypeInfer.aliases.test.cpp | 4 +- tests/TypeInfer.builtins.test.cpp | 21 +- tests/TypeInfer.classes.test.cpp | 56 +++- tests/TypeInfer.generics.test.cpp | 73 ++++- tests/TypeInfer.modules.test.cpp | 4 +- tests/TypeInfer.rwprops.test.cpp | 71 ----- tests/TypeInfer.singletons.test.cpp | 2 +- tests/TypeInfer.tables.test.cpp | 246 +++++++++++---- tests/TypePath.test.cpp | 73 +++-- tests/Unifier2.test.cpp | 8 +- tests/conformance/native.lua | 18 ++ tools/faillist.txt | 63 +--- tools/lldb_formatters.py | 2 +- tools/test_dcr.py | 8 - 62 files changed, 1826 insertions(+), 716 deletions(-) create mode 100644 Analysis/include/Luau/Instantiation2.h create mode 100644 Analysis/src/Instantiation2.cpp delete mode 100644 tests/TypeInfer.rwprops.test.cpp diff --git a/Analysis/include/Luau/Constraint.h b/Analysis/include/Luau/Constraint.h index 38506f0a9..5f9c45726 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 65318a317..3d3dcb4d6 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 0f4be214f..54d229c3f 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 @@ struct ConstraintSolver **/ 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 5a2caf65a..d9b5f1bad 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 000000000..9dfbb613f --- /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 0ca2380ed..8fa4b8b0c 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 @@ struct Subtyping 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 c72864ce0..a0c6b9ccb 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 @@ struct BuiltinTypes 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 79dc435ce..389a2e007 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 6ac4e7c68..2fcc8fef5 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 f9a3fdc96..a0553a2ab 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 6e1fea6aa..94510e323 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 cea810808..cebb226a5 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 1c8620b8a..d5c947da0 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 c0d88d9aa..a8b17c552 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 6d2d04b6c..bf02f743b 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 @@ class TypeCloner2 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 6f7449516..17bffe874 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); - if (generalize && hasFreeType(sig.signature)) - { - TypeId generalizedTy = arena->addType(BlockedType{}); - NotNull gc = addConstraint(sig.signatureScope, func->location, GeneralizationConstraint{generalizedTy, sig.signature}); + 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()); + 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}); + if (auto psc = get(*constraint); psc && psc->returns) + { + if (previous) + constraint->dependencies.push_back(NotNull{previous}); - previous = constraint.get(); - } - }); + previous = constraint.get(); + } + }); + if (generalize && hasFreeType(sig.signature)) + { 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 109fb2fbd..996d638b2 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 ba4e3a35d..d1516b099 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 b0dadd0bd..82b781497 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 235786a8a..525319c6f 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 000000000..31f27f8ef --- /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 720908480..54afd5d66 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 79a7ef566..d79361c02 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 @@ class LintTableLiteral : AstVisitor 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 ee80cc993..62aeb1454 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 d04aeb820..dcfc19655 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); - }); - - if (!(foundPropFromLeftInRight || foundPropFromRightInLeft) && leftTable->props.size() >= 1 && rightTable->props.size() >= 1) + 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) 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 7da9aee85..0deba9bdc 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 3c7c32f14..6abf9a3bb 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 2e2049263..e26ed1387 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()); + emitKey(name); + stringify(prop.type()); + return; + } - // Otherwise emit them separately. - if (auto ty = prop.readType()) + if (prop.readTy) + { + state.emit("read "); + emitKey(name); + stringify(*prop.readTy); + comma = true; + } + if (prop.writeTy) + { + if (comma) { - state.emit("read "); - stringify(*ty); + state.emit(","); + state.newline(); } - if (prop.readType() && prop.writeType()) - state.emit(" + "); - - if (auto ty = prop.writeType()) - { - state.emit("write "); - stringify(*ty); - } + state.emit("write "); + emitKey(name); + stringify(*prop.writeTy); } - else - stringify(prop.type()); + } + + 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 71a07d734..d8044c16e 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,34 +675,35 @@ 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; + if (FFlag::DebugLuauDeferredConstraintResolution) + writeTy = ty; +} + +bool Property::isShared() const +{ + return readTy && writeTy && readTy == writeTy; } -std::optional Property::readType() const +bool Property::isReadOnly() const { - LUAU_ASSERT(FFlag::DebugLuauReadWriteProperties); - LUAU_ASSERT(!(bool(readTy) && bool(writeTy))); - return readTy; + return readTy && !writeTy; } -std::optional Property::writeType() const +bool Property::isWriteOnly() const { - LUAU_ASSERT(FFlag::DebugLuauReadWriteProperties); - LUAU_ASSERT(!(bool(readTy) && bool(writeTy))); - return writeTy; + return !readTy && writeTy; } -bool Property::isShared() const +bool Property::isReadWrite() const { - return readTy && writeTy && readTy == writeTy; + return readTy && writeTy; } TableType::TableType(TableState state, TypeLevel level, Scope* scope) @@ -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 5411bf248..853e46ffd 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 9d28e7daf..5456c4237 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 fb4d68cbc..505072635 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 f746f6209..795324f47 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 5d3dc864b..02e2bf670 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; - - subFn = get(*instantiated); + for (auto generic : subFn->generics) + genericSubstitutions[generic] = freshType(arena, builtinTypes, scope); - 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 fa1ab61ee..a7363552e 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 548d09b9a..2e17bcb51 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 721bb51cb..4b41fc76e 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 2e5cb065c..84e4a654a 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 bada61cd0..bf500d691 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 70fd7fc46..182dd736c 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 1f9aa5a78..5745f6dd9 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 3293b2469..4ce13658d 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 204d4d147..170510bde 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 9cf8c3aa1..b009cec0c 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 b0e01dced..9f7aa77ae 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 2b09603ee..81cb7cee2 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 80a4c058e..d42856c2c 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 c2e4aadd5..b9c0381f3 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 a9f358eb4..52930087e 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 45f5d01c2..11bb5eda6 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 00023b427..0fdea695b 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 8b647f99e..a5f17f774 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 130315602..97efef16f 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 b3ed79475..000000000 --- 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 34fd902b4..81cfabd9c 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 05f6cb7e2..a9ae8fc64 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 697c25400..c5a7a2bb7 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 56edb4a73..842f9e062 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 9df7a3bb0..6ebe85d6b 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 dd51634bf..896757153 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 30654af36..8238cc906 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 3598b02cb..0a23587a5 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)]