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 <aaronweiss@roblox.com>
Co-authored-by: Alexander McCord <amccord@roblox.com>
Co-authored-by: Andy Friesen <afriesen@roblox.com>
Co-authored-by: Aviral Goel <agoel@roblox.com>
Co-authored-by: Vyacheslav Egorov <vegorov@roblox.com>

---------

Co-authored-by: Aaron Weiss <aaronweiss@roblox.com>
Co-authored-by: Alexander McCord <amccord@roblox.com>
Co-authored-by: Andy Friesen <afriesen@roblox.com>
Co-authored-by: Aviral Goel <agoel@roblox.com>
Co-authored-by: David Cope <dcope@roblox.com>
Co-authored-by: Lily Brown <lbrown@roblox.com>
Co-authored-by: Vyacheslav Egorov <vegorov@roblox.com>
This commit is contained in:
Vighnesh-V 2024-02-23 12:08:34 -08:00 committed by GitHub
parent 80928acb92
commit 3b0e93bec9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
62 changed files with 1835 additions and 725 deletions

View file

@ -14,8 +14,16 @@
namespace Luau namespace Luau
{ {
enum class ValueContext;
struct Scope; struct Scope;
// if resultType is a freeType, assignmentType <: freeType <: resultType bounds
struct EqualityConstraint
{
TypeId resultType;
TypeId assignmentType;
};
// subType <: superType // subType <: superType
struct SubtypeConstraint struct SubtypeConstraint
{ {
@ -40,6 +48,8 @@ struct GeneralizationConstraint
{ {
TypeId generalizedType; TypeId generalizedType;
TypeId sourceType; TypeId sourceType;
std::vector<TypeId> interiorTypes;
}; };
// subType ~ inst superType // subType ~ inst superType
@ -145,6 +155,7 @@ struct HasPropConstraint
TypeId resultType; TypeId resultType;
TypeId subjectType; TypeId subjectType;
std::string prop; std::string prop;
ValueContext context;
// HACK: We presently need types like true|false or string|"hello" when // HACK: We presently need types like true|false or string|"hello" when
// deciding whether a particular literal expression should have a singleton // deciding whether a particular literal expression should have a singleton
@ -256,7 +267,8 @@ struct ReducePackConstraint
using ConstraintV = Variant<SubtypeConstraint, PackSubtypeConstraint, GeneralizationConstraint, InstantiationConstraint, IterableConstraint, using ConstraintV = Variant<SubtypeConstraint, PackSubtypeConstraint, GeneralizationConstraint, InstantiationConstraint, IterableConstraint,
NameConstraint, TypeAliasExpansionConstraint, FunctionCallConstraint, FunctionCheckConstraint, PrimitiveTypeConstraint, HasPropConstraint, NameConstraint, TypeAliasExpansionConstraint, FunctionCallConstraint, FunctionCheckConstraint, PrimitiveTypeConstraint, HasPropConstraint,
SetPropConstraint, SetIndexerConstraint, SingletonOrTopTypeConstraint, UnpackConstraint, SetOpConstraint, ReduceConstraint, ReducePackConstraint>; SetPropConstraint, SetIndexerConstraint, SingletonOrTopTypeConstraint, UnpackConstraint, SetOpConstraint, ReduceConstraint, ReducePackConstraint,
EqualityConstraint>;
struct Constraint struct Constraint
{ {

View file

@ -130,6 +130,8 @@ struct ConstraintGenerator
void visitModuleRoot(AstStatBlock* block); void visitModuleRoot(AstStatBlock* block);
private: private:
std::vector<std::vector<TypeId>> interiorTypes;
/** /**
* Fabricates a new free type belonging to a given scope. * Fabricates a new free type belonging to a given scope.
* @param scope the scope the free type belongs to. * @param scope the scope the free type belongs to.

View file

@ -8,6 +8,7 @@
#include "Luau/Location.h" #include "Luau/Location.h"
#include "Luau/Module.h" #include "Luau/Module.h"
#include "Luau/Normalize.h" #include "Luau/Normalize.h"
#include "Luau/Substitution.h"
#include "Luau/ToString.h" #include "Luau/ToString.h"
#include "Luau/Type.h" #include "Luau/Type.h"
#include "Luau/TypeCheckLimits.h" #include "Luau/TypeCheckLimits.h"
@ -20,6 +21,8 @@
namespace Luau namespace Luau
{ {
enum class ValueContext;
struct DcrLogger; struct DcrLogger;
// TypeId, TypePackId, or Constraint*. It is impossible to know which, but we // TypeId, TypePackId, or Constraint*. It is impossible to know which, but we
@ -110,8 +113,6 @@ struct ConstraintSolver
bool isDone(); bool isDone();
void finalizeModule();
/** Attempt to dispatch a constraint. Returns true if it was successful. If /** Attempt to dispatch a constraint. Returns true if it was successful. If
* tryDispatch() returns false, the constraint remains in the unsolved set * tryDispatch() returns false, the constraint remains in the unsolved set
* and will be retried later. * and will be retried later.
@ -136,6 +137,7 @@ struct ConstraintSolver
bool tryDispatch(const SetOpConstraint& c, NotNull<const Constraint> constraint, bool force); bool tryDispatch(const SetOpConstraint& c, NotNull<const Constraint> constraint, bool force);
bool tryDispatch(const ReduceConstraint& c, NotNull<const Constraint> constraint, bool force); bool tryDispatch(const ReduceConstraint& c, NotNull<const Constraint> constraint, bool force);
bool tryDispatch(const ReducePackConstraint& c, NotNull<const Constraint> constraint, bool force); bool tryDispatch(const ReducePackConstraint& c, NotNull<const Constraint> constraint, bool force);
bool tryDispatch(const EqualityConstraint& c, NotNull<const Constraint> constraint, bool force);
// for a, ... in some_table do // for a, ... in some_table do
// also handles __iter metamethod // also handles __iter metamethod
@ -146,9 +148,9 @@ struct ConstraintSolver
TypeId nextTy, TypeId tableTy, TypeId firstIndexTy, const IterableConstraint& c, NotNull<const Constraint> constraint, bool force); TypeId nextTy, TypeId tableTy, TypeId firstIndexTy, const IterableConstraint& c, NotNull<const Constraint> constraint, bool force);
std::pair<std::vector<TypeId>, std::optional<TypeId>> lookupTableProp( std::pair<std::vector<TypeId>, std::optional<TypeId>> lookupTableProp(
TypeId subjectType, const std::string& propName, bool suppressSimplification = false); TypeId subjectType, const std::string& propName, ValueContext context, bool suppressSimplification = false);
std::pair<std::vector<TypeId>, std::optional<TypeId>> lookupTableProp( std::pair<std::vector<TypeId>, std::optional<TypeId>> lookupTableProp(
TypeId subjectType, const std::string& propName, bool suppressSimplification, DenseHashSet<TypeId>& seen); TypeId subjectType, const std::string& propName, ValueContext context, bool suppressSimplification, DenseHashSet<TypeId>& seen);
void block(NotNull<const Constraint> target, NotNull<const Constraint> constraint); void block(NotNull<const Constraint> target, NotNull<const Constraint> constraint);
/** /**
@ -208,23 +210,6 @@ struct ConstraintSolver
*/ */
bool isBlocked(NotNull<const Constraint> constraint); bool isBlocked(NotNull<const Constraint> 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<TypeError> unify(NotNull<Scope> 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> scope, Location location, TypePackId subPack, TypePackId superPack);
/** Pushes a new solver constraint to the solver. /** Pushes a new solver constraint to the solver.
* @param cv the body of the constraint. * @param cv the body of the constraint.
**/ **/
@ -253,20 +238,33 @@ struct ConstraintSolver
*/ */
bool hasUnresolvedConstraints(TypeId ty); bool hasUnresolvedConstraints(TypeId ty);
private: /**
/** Helper used by tryDispatch(SubtypeConstraint) and * Creates a new Unifier and performs a single unification operation.
* tryDispatch(PackSubtypeConstraint)
* *
* 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 <typename TID>
bool unify(NotNull<Scope> 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. * 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 fails, replace all free types with errorType.
* *
* If unification succeeds, unblock every type changed by the unification. * If unification succeeds, unblock every type changed by the unification.
*
* @returns true if the unification succeeded. False if the unification was
* too complex.
*/ */
template<typename TID> template<typename TID>
bool tryUnify(NotNull<const Constraint> constraint, TID subTy, TID superTy); bool unify(NotNull<const Constraint> constraint, TID subTy, TID superTy);
private:
/** /**
* Bind a BlockedType to another type while taking care not to bind it to * 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 * itself in the case that resultTy == blockedTy. This can happen if we
@ -295,6 +293,13 @@ private:
**/ **/
void unblock_(BlockedConstraintId progressed); 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> scope, const Location& location, const Substitution& subst);
TypeId errorRecoveryType() const; TypeId errorRecoveryType() const;
TypePackId errorRecoveryTypePack() const; TypePackId errorRecoveryTypePack() const;

View file

@ -380,6 +380,20 @@ struct NonStrictFunctionDefinitionError
bool operator==(const NonStrictFunctionDefinitionError& rhs) const; bool operator==(const NonStrictFunctionDefinitionError& rhs) const;
}; };
struct PropertyAccessViolation
{
TypeId table;
Name key;
enum
{
CannotRead,
CannotWrite
} context;
bool operator==(const PropertyAccessViolation& rhs) const;
};
struct CheckedFunctionIncorrectArgs struct CheckedFunctionIncorrectArgs
{ {
std::string functionName; std::string functionName;
@ -388,14 +402,28 @@ struct CheckedFunctionIncorrectArgs
bool operator==(const CheckedFunctionIncorrectArgs& rhs) const; bool operator==(const CheckedFunctionIncorrectArgs& rhs) const;
}; };
using TypeErrorData = struct UnexpectedTypeInSubtyping
Variant<TypeMismatch, UnknownSymbol, UnknownProperty, NotATable, CannotExtendTable, OnlyTablesCanHaveMethods, DuplicateTypeDefinition, {
CountMismatch, FunctionDoesNotTakeSelf, FunctionRequiresSelf, OccursCheckFailed, UnknownRequire, IncorrectGenericParameterCount, SyntaxError, TypeId ty;
CodeTooComplex, UnificationTooComplex, UnknownPropButFoundLikeProp, GenericError, InternalError, CannotCallNonFunction, ExtraInformation,
DeprecatedApiUsed, ModuleHasCyclicDependency, IllegalRequire, FunctionExitsWithoutReturning, DuplicateGenericParameter, bool operator==(const UnexpectedTypeInSubtyping& rhs) const;
CannotInferBinaryOperation, MissingProperties, SwappedGenericTypeParameter, OptionalValueAccess, MissingUnionProperty, TypesAreUnrelated, };
NormalizationTooComplex, TypePackMismatch, DynamicPropertyLookupOnClassesUnsafe, UninhabitedTypeFamily, UninhabitedTypePackFamily,
WhereClauseNeeded, PackWhereClauseNeeded, CheckedFunctionCallError, NonStrictFunctionDefinitionError, CheckedFunctionIncorrectArgs>; struct UnexpectedTypePackInSubtyping
{
TypePackId tp;
bool operator==(const UnexpectedTypePackInSubtyping& rhs) const;
};
using TypeErrorData = Variant<TypeMismatch, UnknownSymbol, UnknownProperty, NotATable, CannotExtendTable, OnlyTablesCanHaveMethods,
DuplicateTypeDefinition, CountMismatch, FunctionDoesNotTakeSelf, FunctionRequiresSelf, OccursCheckFailed, UnknownRequire,
IncorrectGenericParameterCount, SyntaxError, CodeTooComplex, UnificationTooComplex, UnknownPropButFoundLikeProp, GenericError, InternalError,
CannotCallNonFunction, ExtraInformation, DeprecatedApiUsed, ModuleHasCyclicDependency, IllegalRequire, FunctionExitsWithoutReturning,
DuplicateGenericParameter, CannotInferBinaryOperation, MissingProperties, SwappedGenericTypeParameter, OptionalValueAccess, MissingUnionProperty,
TypesAreUnrelated, NormalizationTooComplex, TypePackMismatch, DynamicPropertyLookupOnClassesUnsafe, UninhabitedTypeFamily,
UninhabitedTypePackFamily, WhereClauseNeeded, PackWhereClauseNeeded, CheckedFunctionCallError, NonStrictFunctionDefinitionError,
PropertyAccessViolation, CheckedFunctionIncorrectArgs, UnexpectedTypeInSubtyping, UnexpectedTypePackInSubtyping>;
struct TypeErrorSummary struct TypeErrorSummary
{ {

View file

@ -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<TypeId, TypeId> replacements;
DenseHashMap<TypePackId, TypePackId> replacementPacks;
Replacer(NotNull<TypeArena> arena, DenseHashMap<TypeId, TypeId> replacements, DenseHashMap<TypePackId, TypePackId> 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<TypeId, TypeId> genericSubstitutions{nullptr};
// Mapping from generic type packs to `TypePack`s of free types to be used in instantiation.
DenseHashMap<TypePackId, TypePackId> genericPackSubstitutions{nullptr};
Instantiation2(TypeArena* arena, DenseHashMap<TypeId, TypeId> genericSubstitutions, DenseHashMap<TypePackId, TypePackId> 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

View file

@ -21,14 +21,15 @@ struct InternalErrorReporter;
class TypeIds; class TypeIds;
class Normalizer; class Normalizer;
struct NormalizedType;
struct NormalizedClassType; struct NormalizedClassType;
struct NormalizedStringType;
struct NormalizedFunctionType; struct NormalizedFunctionType;
struct TypeArena; struct NormalizedStringType;
struct TypeCheckLimits; struct NormalizedType;
struct Property;
struct Scope; struct Scope;
struct TableIndexer; struct TableIndexer;
struct TypeArena;
struct TypeCheckLimits;
enum class SubtypingVariance enum class SubtypingVariance
{ {
@ -79,6 +80,7 @@ struct SubtypingResult
SubtypingResult& withSubPath(TypePath::Path path); SubtypingResult& withSubPath(TypePath::Path path);
SubtypingResult& withSuperPath(TypePath::Path path); SubtypingResult& withSuperPath(TypePath::Path path);
SubtypingResult& withErrors(ErrorVec& err); SubtypingResult& withErrors(ErrorVec& err);
SubtypingResult& withError(TypeError err);
// Only negates the `isSubtype`. // Only negates the `isSubtype`.
static SubtypingResult negate(const SubtypingResult& result); static SubtypingResult negate(const SubtypingResult& result);
@ -102,6 +104,10 @@ struct SubtypingEnvironment
DenseHashMap<TypePackId, TypePackId> mappedGenericPacks{nullptr}; DenseHashMap<TypePackId, TypePackId> mappedGenericPacks{nullptr};
DenseHashMap<std::pair<TypeId, TypeId>, SubtypingResult, TypePairHash> ephemeralCache{{}}; DenseHashMap<std::pair<TypeId, TypeId>, SubtypingResult, TypePairHash> ephemeralCache{{}};
/// Applies `mappedGenerics` to the given type.
/// This is used specifically to substitute for generics in type family instances.
std::optional<TypeId> applyMappedGenerics(NotNull<BuiltinTypes> builtinTypes, NotNull<TypeArena> arena, TypeId ty);
}; };
struct Subtyping struct Subtyping
@ -192,6 +198,7 @@ private:
SubtypingResult isCovariantWith(SubtypingEnvironment& env, const SingletonType* subSingleton, const TableType* superTable); 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 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 NormalizedType* subNorm, const NormalizedType* superNorm);
SubtypingResult isCovariantWith(SubtypingEnvironment& env, const NormalizedClassType& subClass, const NormalizedClassType& superClass); SubtypingResult isCovariantWith(SubtypingEnvironment& env, const NormalizedClassType& subClass, const NormalizedClassType& superClass);

View file

@ -414,16 +414,11 @@ struct Property
TypeId type() const; TypeId type() const;
void setType(TypeId ty); 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<TypeId> readType() const;
std::optional<TypeId> writeType() const;
bool isShared() const; bool isShared() const;
bool isReadOnly() const;
bool isWriteOnly() const;
bool isReadWrite() const;
private:
std::optional<TypeId> readTy; std::optional<TypeId> readTy;
std::optional<TypeId> writeTy; std::optional<TypeId> writeTy;
}; };
@ -844,6 +839,7 @@ public:
const TypePackId emptyTypePack; const TypePackId emptyTypePack;
const TypePackId anyTypePack; const TypePackId anyTypePack;
const TypePackId unknownTypePack;
const TypePackId neverTypePack; const TypePackId neverTypePack;
const TypePackId uninhabitableTypePack; const TypePackId uninhabitableTypePack;
const TypePackId errorTypePack; const TypePackId errorTypePack;

View file

@ -30,8 +30,10 @@ struct TypeFamilyContext
// nullptr if the type family is being reduced outside of the constraint solver. // nullptr if the type family is being reduced outside of the constraint solver.
ConstraintSolver* solver; ConstraintSolver* solver;
// The constraint being reduced in this run of the reduction
const Constraint* constraint;
TypeFamilyContext(NotNull<ConstraintSolver> cs, NotNull<Scope> scope) TypeFamilyContext(NotNull<ConstraintSolver> cs, NotNull<Scope> scope, NotNull<const Constraint> constraint)
: arena(cs->arena) : arena(cs->arena)
, builtins(cs->builtinTypes) , builtins(cs->builtinTypes)
, scope(scope) , scope(scope)
@ -39,6 +41,7 @@ struct TypeFamilyContext
, ice(NotNull{&cs->iceReporter}) , ice(NotNull{&cs->iceReporter})
, limits(NotNull{&cs->limits}) , limits(NotNull{&cs->limits})
, solver(cs.get()) , solver(cs.get())
, constraint(constraint.get())
{ {
} }
@ -51,10 +54,10 @@ struct TypeFamilyContext
, ice(ice) , ice(ice)
, limits(limits) , limits(limits)
, solver(nullptr) , solver(nullptr)
, constraint(nullptr)
{ {
} }
}; };
/// Represents a reduction result, which may have successfully reduced the type, /// Represents a reduction result, which may have successfully reduced the type,
/// may have concretely failed to reduce the type, or may simply be stuck /// may have concretely failed to reduce the type, or may simply be stuck
/// without more information. /// without more information.

View file

@ -28,6 +28,8 @@ std::optional<TypeId> findMetatableEntry(
NotNull<BuiltinTypes> builtinTypes, ErrorVec& errors, TypeId type, const std::string& entry, Location location); NotNull<BuiltinTypes> builtinTypes, ErrorVec& errors, TypeId type, const std::string& entry, Location location);
std::optional<TypeId> findTablePropertyRespectingMeta( std::optional<TypeId> findTablePropertyRespectingMeta(
NotNull<BuiltinTypes> builtinTypes, ErrorVec& errors, TypeId ty, const std::string& name, Location location); NotNull<BuiltinTypes> builtinTypes, ErrorVec& errors, TypeId ty, const std::string& name, Location location);
std::optional<TypeId> findTablePropertyRespectingMeta(
NotNull<BuiltinTypes> 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. // Returns the minimum and maximum number of types the argument list can accept.
std::pair<size_t, std::optional<size_t>> getParameterExtents(const TxnLog* log, TypePackId tp, bool includeHiddenVariadics = false); std::pair<size_t, std::optional<size_t>> getParameterExtents(const TxnLog* log, TypePackId tp, bool includeHiddenVariadics = false);

View file

@ -38,6 +38,11 @@ struct Unifier2
DenseHashMap<TypeId, std::vector<TypeId>> expandedFreeTypes{nullptr}; DenseHashMap<TypeId, std::vector<TypeId>> expandedFreeTypes{nullptr};
// Mapping from generic types to free types to be used in instantiation.
DenseHashMap<TypeId, TypeId> genericSubstitutions{nullptr};
// Mapping from generic type packs to `TypePack`s of free types to be used in instantiation.
DenseHashMap<TypePackId, TypePackId> genericPackSubstitutions{nullptr};
int recursionCount = 0; int recursionCount = 0;
int recursionLimit = 0; int recursionLimit = 0;

View file

@ -11,7 +11,6 @@
LUAU_FASTINT(LuauVisitRecursionLimit) LUAU_FASTINT(LuauVisitRecursionLimit)
LUAU_FASTFLAG(LuauBoundLazyTypes2) LUAU_FASTFLAG(LuauBoundLazyTypes2)
LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution) LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution)
LUAU_FASTFLAG(DebugLuauReadWriteProperties)
namespace Luau namespace Luau
{ {
@ -281,12 +280,16 @@ struct GenericTypeVisitor
{ {
for (auto& [_name, prop] : ttv->props) for (auto& [_name, prop] : ttv->props)
{ {
if (FFlag::DebugLuauReadWriteProperties) if (FFlag::DebugLuauDeferredConstraintResolution)
{ {
if (auto ty = prop.readType()) if (auto ty = prop.readTy)
traverse(*ty); 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); traverse(*ty);
} }
else else
@ -315,12 +318,16 @@ struct GenericTypeVisitor
{ {
for (const auto& [name, prop] : ctv->props) 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); 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); traverse(*ty);
} }
else else

View file

@ -11,7 +11,7 @@
#include <algorithm> #include <algorithm>
LUAU_FASTFLAG(DebugLuauReadWriteProperties) LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution);
namespace Luau namespace Luau
{ {
@ -524,9 +524,9 @@ std::optional<DocumentationSymbol> getDocumentationSymbolAtPosition(const Source
{ {
if (auto propIt = ttv->props.find(indexName->index.value); propIt != ttv->props.end()) 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); return checkOverloadedDocumentationSymbol(module, *ty, parentExpr, propIt->second.documentationSymbol);
} }
else else
@ -537,9 +537,9 @@ std::optional<DocumentationSymbol> getDocumentationSymbolAtPosition(const Source
{ {
if (auto propIt = ctv->props.find(indexName->index.value); propIt != ctv->props.end()) 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); return checkOverloadedDocumentationSymbol(module, *ty, parentExpr, propIt->second.documentationSymbol);
} }
else else

View file

@ -14,7 +14,6 @@
#include <utility> #include <utility>
LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution); LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution);
LUAU_FASTFLAG(DebugLuauReadWriteProperties);
LUAU_FASTFLAGVARIABLE(LuauAutocompleteStringLiteralBounds, false); LUAU_FASTFLAGVARIABLE(LuauAutocompleteStringLiteralBounds, false);
LUAU_FASTFLAGVARIABLE(LuauAutocompleteTableKeysNoInitialCharacter, false); LUAU_FASTFLAGVARIABLE(LuauAutocompleteTableKeysNoInitialCharacter, false);
@ -277,9 +276,9 @@ static void autocompleteProps(const Module& module, TypeArena* typeArena, NotNul
{ {
Luau::TypeId type; Luau::TypeId type;
if (FFlag::DebugLuauReadWriteProperties) if (FFlag::DebugLuauDeferredConstraintResolution)
{ {
if (auto ty = prop.readType()) if (auto ty = prop.readTy)
type = follow(*ty); type = follow(*ty);
else else
continue; continue;

View file

@ -283,6 +283,26 @@ void registerBuiltinGlobals(Frontend& frontend, GlobalTypes& globals, bool typeC
} }
attachMagicFunction(getGlobalBinding(globals, "assert"), magicFunctionAssert); attachMagicFunction(getGlobalBinding(globals, "assert"), magicFunctionAssert);
if (FFlag::DebugLuauDeferredConstraintResolution)
{
// declare function assert<T>(value: T, errorMessage: string?): intersect<T, ~(false?)>
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, "setmetatable"), magicFunctionSetMetaTable);
attachMagicFunction(getGlobalBinding(globals, "select"), magicFunctionSelect); attachMagicFunction(getGlobalBinding(globals, "select"), magicFunctionSelect);
attachDcrMagicFunction(getGlobalBinding(globals, "select"), dcrMagicFunctionSelect); attachDcrMagicFunction(getGlobalBinding(globals, "select"), dcrMagicFunctionSelect);

View file

@ -8,8 +8,6 @@
#include "Luau/TypePack.h" #include "Luau/TypePack.h"
#include "Luau/Unifiable.h" #include "Luau/Unifiable.h"
LUAU_FASTFLAG(DebugLuauReadWriteProperties)
LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution) LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution)
LUAU_FASTINTVARIABLE(LuauTypeCloneRecursionLimit, 300) LUAU_FASTINTVARIABLE(LuauTypeCloneRecursionLimit, 300)
@ -196,14 +194,14 @@ private:
Property shallowClone(const Property& p) Property shallowClone(const Property& p)
{ {
if (FFlag::DebugLuauReadWriteProperties) if (FFlag::DebugLuauDeferredConstraintResolution)
{ {
std::optional<TypeId> cloneReadTy; std::optional<TypeId> cloneReadTy;
if (auto ty = p.readType()) if (auto ty = p.readTy)
cloneReadTy = shallowClone(*ty); cloneReadTy = shallowClone(*ty);
std::optional<TypeId> cloneWriteTy; std::optional<TypeId> cloneWriteTy;
if (auto ty = p.writeType()) if (auto ty = p.writeTy)
cloneWriteTy = shallowClone(*ty); cloneWriteTy = shallowClone(*ty);
std::optional<Property> cloned = Property::create(cloneReadTy, cloneWriteTy); std::optional<Property> cloned = Property::create(cloneReadTy, cloneWriteTy);
@ -460,14 +458,14 @@ namespace
Property clone(const Property& prop, TypeArena& dest, CloneState& cloneState) Property clone(const Property& prop, TypeArena& dest, CloneState& cloneState)
{ {
if (FFlag::DebugLuauReadWriteProperties) if (FFlag::DebugLuauDeferredConstraintResolution)
{ {
std::optional<TypeId> cloneReadTy; std::optional<TypeId> cloneReadTy;
if (auto ty = prop.readType()) if (auto ty = prop.readTy)
cloneReadTy = clone(*ty, dest, cloneState); cloneReadTy = clone(*ty, dest, cloneState);
std::optional<TypeId> cloneWriteTy; std::optional<TypeId> cloneWriteTy;
if (auto ty = prop.writeType()) if (auto ty = prop.writeTy)
cloneWriteTy = clone(*ty, dest, cloneState); cloneWriteTy = clone(*ty, dest, cloneState);
std::optional<Property> cloned = Property::create(cloneReadTy, cloneWriteTy); std::optional<Property> cloned = Property::create(cloneReadTy, cloneWriteTy);

View file

@ -217,9 +217,25 @@ void ConstraintGenerator::visitModuleRoot(AstStatBlock* block)
rootScope->returnType = freshTypePack(scope); rootScope->returnType = freshTypePack(scope);
TypeId moduleFnTy = arena->addType(FunctionType{TypeLevel{}, rootScope, builtinTypes->anyTypePack, rootScope->returnType});
interiorTypes.emplace_back();
prepopulateGlobalScope(scope, block); 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<Constraint> 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); fillInInferredBindings(scope, block);
@ -406,7 +422,7 @@ void ConstraintGenerator::computeRefinement(const ScopePtr& scope, Location loca
TypeId nextDiscriminantTy = arena->addType(TableType{}); TypeId nextDiscriminantTy = arena->addType(TableType{});
NotNull<TableType> table{getMutable<TableType>(nextDiscriminantTy)}; NotNull<TableType> table{getMutable<TableType>(nextDiscriminantTy)};
table->props[*key->propName] = {discriminantTy}; table->props[*key->propName] = Property::readonly(discriminantTy);
table->scope = scope.get(); table->scope = scope.get();
table->state = TableState::Sealed; table->state = TableState::Sealed;
@ -1894,7 +1910,7 @@ Inference ConstraintGenerator::checkIndexName(const ScopePtr& scope, const Refin
scope->rvalueRefinements[key->def] = result; 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) if (key)
return Inference{result, refinementArena.proposition(key, builtinTypes->truthyType)}; return Inference{result, refinementArena.proposition(key, builtinTypes->truthyType)};
@ -1945,27 +1961,30 @@ Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprFunction* fun
{ {
Checkpoint startCheckpoint = checkpoint(this); Checkpoint startCheckpoint = checkpoint(this);
FunctionSignature sig = checkFunctionSignature(scope, func, expectedType); FunctionSignature sig = checkFunctionSignature(scope, func, expectedType);
interiorTypes.push_back(std::vector<TypeId>{});
checkFunctionBody(sig.bodyScope, func); checkFunctionBody(sig.bodyScope, func);
Checkpoint endCheckpoint = checkpoint(this); Checkpoint endCheckpoint = checkpoint(this);
TypeId generalizedTy = arena->addType(BlockedType{});
NotNull<Constraint> gc = addConstraint(sig.signatureScope, func->location, GeneralizationConstraint{generalizedTy, sig.signature, std::move(interiorTypes.back())});
interiorTypes.pop_back();
Constraint* previous = nullptr;
forEachConstraint(startCheckpoint, endCheckpoint, this, [gc, &previous](const ConstraintPtr& constraint) {
gc->dependencies.emplace_back(constraint.get());
if (auto psc = get<PackSubtypeConstraint>(*constraint); psc && psc->returns)
{
if (previous)
constraint->dependencies.push_back(NotNull{previous});
previous = constraint.get();
}
});
if (generalize && hasFreeType(sig.signature)) if (generalize && hasFreeType(sig.signature))
{ {
TypeId generalizedTy = arena->addType(BlockedType{});
NotNull<Constraint> gc = addConstraint(sig.signatureScope, func->location, GeneralizationConstraint{generalizedTy, sig.signature});
Constraint* previous = nullptr;
forEachConstraint(startCheckpoint, endCheckpoint, this, [gc, &previous](const ConstraintPtr& constraint) {
gc->dependencies.emplace_back(constraint.get());
if (auto psc = get<PackSubtypeConstraint>(*constraint); psc && psc->returns)
{
if (previous)
constraint->dependencies.push_back(NotNull{previous});
previous = constraint.get();
}
});
return Inference{generalizedTy}; return Inference{generalizedTy};
} }
else else
@ -2529,7 +2548,8 @@ TypeId ConstraintGenerator::updateProperty(const ScopePtr& scope, AstExpr* expr,
{ {
TypeId segmentTy = arena->addType(BlockedType{}); TypeId segmentTy = arena->addType(BlockedType{});
module->astTypes[exprs[i]] = segmentTy; 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; prevSegmentTy = segmentTy;
} }
@ -2563,6 +2583,8 @@ Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprTable* expr,
ttv->state = TableState::Unsealed; ttv->state = TableState::Unsealed;
ttv->scope = scope.get(); ttv->scope = scope.get();
interiorTypes.back().push_back(ty);
auto createIndexer = [this, scope, ttv](const Location& location, TypeId currentIndexType, TypeId currentResultType) { auto createIndexer = [this, scope, ttv](const Location& location, TypeId currentIndexType, TypeId currentResultType) {
if (!ttv->indexer) if (!ttv->indexer)
{ {
@ -2613,7 +2635,8 @@ Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprTable* expr,
{ {
expectedValueType = arena->addType(BlockedType{}); expectedValueType = arena->addType(BlockedType{});
addConstraint(scope, item.value->location, 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) 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 it is possible for execution to reach the end of the function, the return type must be compatible with ()
ControlFlow cf = visitBlockWithoutChildScope(scope, fn->body);
if (nullptr != getFallthrough(fn->body)) if (cf == ControlFlow::None)
{ addConstraint(scope, fn->location, PackSubtypeConstraint{builtinTypes->emptyTypePack, scope->returnType});
TypePackId empty = arena->addTypePack({}); // TODO we could have CG retain one of these forever
addConstraint(scope, fn->location, PackSubtypeConstraint{scope->returnType, empty});
}
} }
TypeId ConstraintGenerator::resolveType(const ScopePtr& scope, AstType* ty, bool inTypeArguments, bool replaceErrorWithFresh) 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) for (const AstTableProp& prop : tab->props)
{ {
if (prop.access == AstTableAccess::Read) // TODO: Recursion limit.
reportError(prop.accessLocation.value_or(Location{}), GenericError{"read keyword is illegal here"}); TypeId propTy = resolveType(scope, prop.type, inTypeArguments);
else if (prop.access == AstTableAccess::Write)
reportError(prop.accessLocation.value_or(Location{}), GenericError{"write keyword is illegal here"}); Property& p = props[prop.name.value];
else if (prop.access == AstTableAccess::ReadWrite) p.typeLocation = prop.location;
switch (prop.access)
{ {
std::string name = prop.name.value; case AstTableAccess::ReadWrite:
// TODO: Recursion limit. p.readTy = propTy;
TypeId propTy = resolveType(scope, prop.type, inTypeArguments); p.writeTy = propTy;
props[name] = {propTy}; break;
props[name].typeLocation = prop.location; case AstTableAccess::Read:
} p.readTy = propTy;
else 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))); ice->ice("Unexpected property access " + std::to_string(int(prop.access)));
break;
}
} }
if (AstTableIndexer* astIndexer = tab->indexer) if (AstTableIndexer* astIndexer = tab->indexer)

View file

@ -6,6 +6,7 @@
#include "Luau/ConstraintSolver.h" #include "Luau/ConstraintSolver.h"
#include "Luau/DcrLogger.h" #include "Luau/DcrLogger.h"
#include "Luau/Instantiation.h" #include "Luau/Instantiation.h"
#include "Luau/Instantiation2.h"
#include "Luau/Location.h" #include "Luau/Location.h"
#include "Luau/ModuleResolver.h" #include "Luau/ModuleResolver.h"
#include "Luau/OverloadResolution.h" #include "Luau/OverloadResolution.h"
@ -430,8 +431,6 @@ void ConstraintSolver::run()
progress |= runSolverPass(true); progress |= runSolverPass(true);
} while (progress); } while (progress);
finalizeModule();
if (FFlag::DebugLuauLogSolver) if (FFlag::DebugLuauLogSolver)
{ {
dumpBindings(rootScope, opts); dumpBindings(rootScope, opts);
@ -477,48 +476,6 @@ struct FreeTypeSearcher : TypeOnceVisitor
} // namespace } // namespace
void ConstraintSolver::finalizeModule()
{
Anyification a{arena, rootScope, builtinTypes, &iceReporter, builtinTypes->anyType, builtinTypes->anyTypePack};
std::optional<TypePackId> 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<TypeAndLocation> queue;
for (auto& [name, binding] : rootScope->bindings)
queue.push_back({binding.typeId, binding.location});
DenseHashSet<TypeId> 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<const Constraint> constraint, bool force) bool ConstraintSolver::tryDispatch(NotNull<const Constraint> constraint, bool force)
{ {
if (!force && isBlocked(constraint)) if (!force && isBlocked(constraint))
@ -562,6 +519,8 @@ bool ConstraintSolver::tryDispatch(NotNull<const Constraint> constraint, bool fo
success = tryDispatch(*rc, constraint, force); success = tryDispatch(*rc, constraint, force);
else if (auto rpc = get<ReducePackConstraint>(*constraint)) else if (auto rpc = get<ReducePackConstraint>(*constraint))
success = tryDispatch(*rpc, constraint, force); success = tryDispatch(*rpc, constraint, force);
else if (auto eqc = get<EqualityConstraint>(*constraint))
success = tryDispatch(*eqc, constraint, force);
else else
LUAU_ASSERT(false); LUAU_ASSERT(false);
@ -578,7 +537,9 @@ bool ConstraintSolver::tryDispatch(const SubtypeConstraint& c, NotNull<const Con
else if (isBlocked(c.superType)) else if (isBlocked(c.superType))
return block(c.superType, constraint); return block(c.superType, constraint);
return tryUnify(constraint, c.subType, c.superType); unify(constraint, c.subType, c.superType);
return true;
} }
bool ConstraintSolver::tryDispatch(const PackSubtypeConstraint& c, NotNull<const Constraint> constraint, bool force) bool ConstraintSolver::tryDispatch(const PackSubtypeConstraint& c, NotNull<const Constraint> constraint, bool force)
@ -588,7 +549,9 @@ bool ConstraintSolver::tryDispatch(const PackSubtypeConstraint& c, NotNull<const
else if (isBlocked(c.superPack)) else if (isBlocked(c.superPack))
return block(c.superPack, constraint); return block(c.superPack, constraint);
return tryUnify(constraint, c.subPack, c.superPack); unify(constraint, c.subPack, c.superPack);
return true;
} }
bool ConstraintSolver::tryDispatch(const GeneralizationConstraint& c, NotNull<const Constraint> constraint, bool force) bool ConstraintSolver::tryDispatch(const GeneralizationConstraint& c, NotNull<const Constraint> constraint, bool force)
@ -615,13 +578,13 @@ bool ConstraintSolver::tryDispatch(const GeneralizationConstraint& c, NotNull<co
if (get<BlockedType>(generalizedType)) if (get<BlockedType>(generalizedType))
asMutable(generalizedType)->ty.emplace<BoundType>(generalized->result); asMutable(generalizedType)->ty.emplace<BoundType>(generalized->result);
else else
unify(constraint->scope, constraint->location, generalizedType, generalized->result); unify(constraint, generalizedType, generalized->result);
for (auto [free, gen] : generalized->insertedGenerics.pairings) 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) for (auto [free, gen] : generalized->insertedGenericPacks.pairings)
unify(constraint->scope, constraint->location, free, gen); unify(constraint, free, gen);
} }
else else
{ {
@ -632,6 +595,9 @@ bool ConstraintSolver::tryDispatch(const GeneralizationConstraint& c, NotNull<co
unblock(c.generalizedType, constraint->location); unblock(c.generalizedType, constraint->location);
unblock(c.sourceType, constraint->location); unblock(c.sourceType, constraint->location);
for (TypeId ty : c.interiorTypes)
u2.generalize(ty);
return true; return true;
} }
@ -730,7 +696,7 @@ bool ConstraintSolver::tryDispatch(const IterableConstraint& c, NotNull<const Co
Anyification anyify{arena, constraint->scope, builtinTypes, &iceReporter, errorRecoveryType(), errorRecoveryTypePack()}; Anyification anyify{arena, constraint->scope, builtinTypes, &iceReporter, errorRecoveryType(), errorRecoveryTypePack()};
std::optional<TypePackId> anyified = anyify.substitute(c.variables); std::optional<TypePackId> anyified = anyify.substitute(c.variables);
LUAU_ASSERT(anyified); LUAU_ASSERT(anyified);
unify(constraint->scope, constraint->location, *anyified, c.variables); unify(constraint, *anyified, c.variables);
return true; return true;
} }
@ -1112,6 +1078,26 @@ bool ConstraintSolver::tryDispatch(const FunctionCallConstraint& c, NotNull<cons
const bool occursCheckPassed = u2.unify(overloadToUse, inferredTy); const bool occursCheckPassed = u2.unify(overloadToUse, inferredTy);
if (!u2.genericSubstitutions.empty() || !u2.genericPackSubstitutions.empty())
{
Instantiation2 instantiation{arena, std::move(u2.genericSubstitutions), std::move(u2.genericPackSubstitutions)};
std::optional<TypePackId> 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<BoundTypePack>(result);
}
for (const auto& [expanded, additions] : u2.expandedFreeTypes) for (const auto& [expanded, additions] : u2.expandedFreeTypes)
{ {
for (TypeId addition : additions) for (TypeId addition : additions)
@ -1132,7 +1118,7 @@ bool ConstraintSolver::tryDispatch(const FunctionCallConstraint& c, NotNull<cons
bool ConstraintSolver::tryDispatch(const FunctionCheckConstraint& c, NotNull<const Constraint> constraint) bool ConstraintSolver::tryDispatch(const FunctionCheckConstraint& c, NotNull<const Constraint> constraint)
{ {
const TypeId fn = follow(c.fn); TypeId fn = follow(c.fn);
const TypePackId argsPack = follow(c.argsPack); const TypePackId argsPack = follow(c.argsPack);
if (isBlocked(fn)) if (isBlocked(fn))
@ -1156,6 +1142,35 @@ bool ConstraintSolver::tryDispatch(const FunctionCheckConstraint& c, NotNull<con
if (!ftv) if (!ftv)
return true; return true;
DenseHashMap<TypeId, TypeId> replacements{nullptr};
DenseHashMap<TypePackId, TypePackId> 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<TypeId> res = replacer.substitute(fn);
if (res)
{
fn = *res;
ftv = get<FunctionType>(*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<TypeId> expectedArgs = flatten(ftv->argTypes).first; const std::vector<TypeId> expectedArgs = flatten(ftv->argTypes).first;
const std::vector<TypeId> argPackHead = flatten(argsPack).first; const std::vector<TypeId> argPackHead = flatten(argsPack).first;
@ -1237,7 +1252,7 @@ bool ConstraintSolver::tryDispatch(const HasPropConstraint& c, NotNull<const Con
if (isBlocked(subjectType) || get<PendingExpansionType>(subjectType) || get<TypeFamilyInstanceType>(subjectType)) if (isBlocked(subjectType) || get<PendingExpansionType>(subjectType) || get<TypeFamilyInstanceType>(subjectType))
return block(subjectType, constraint); 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()) if (!blocked.empty())
{ {
for (TypeId blocked : blocked) for (TypeId blocked : blocked)
@ -1304,7 +1319,7 @@ static void updateTheTableType(
for (size_t i = 0; i < path.size() - 1; ++i) for (size_t i = 0; i < path.size() - 1; ++i)
{ {
t = follow(t); 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(); dummy.clear();
if (!propTy) if (!propTy)
@ -1329,17 +1344,26 @@ static void updateTheTableType(
bool ConstraintSolver::tryDispatch(const SetPropConstraint& c, NotNull<const Constraint> constraint, bool force) bool ConstraintSolver::tryDispatch(const SetPropConstraint& c, NotNull<const Constraint> constraint, bool force)
{ {
TypeId subjectType = follow(c.subjectType); TypeId subjectType = follow(c.subjectType);
const TypeId propType = follow(c.propType);
if (isBlocked(subjectType)) if (isBlocked(subjectType))
return block(subjectType, constraint); return block(subjectType, constraint);
std::optional<TypeId> existingPropType = subjectType; std::optional<TypeId> 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) if (!existingPropType)
break; 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()) if (!blocked.empty())
{ {
for (TypeId blocked : blocked) for (TypeId blocked : blocked)
@ -1356,8 +1380,8 @@ bool ConstraintSolver::tryDispatch(const SetPropConstraint& c, NotNull<const Con
if (existingPropType) if (existingPropType)
{ {
if (!isBlocked(c.propType)) unify(constraint->scope, constraint->location, propType, *existingPropType);
unify(constraint->scope, constraint->location, c.propType, *existingPropType); unify(constraint->scope, constraint->location, *existingPropType, propType);
bind(c.resultType, c.subjectType); bind(c.resultType, c.subjectType);
unblock(c.resultType, constraint->location); unblock(c.resultType, constraint->location);
return true; return true;
@ -1382,7 +1406,7 @@ bool ConstraintSolver::tryDispatch(const SetPropConstraint& c, NotNull<const Con
{ {
LUAU_ASSERT(!subjectType->persistent); LUAU_ASSERT(!subjectType->persistent);
ttv->props[c.path[0]] = Property{c.propType}; ttv->props[c.path[0]] = Property{propType};
bind(c.resultType, subjectType); bind(c.resultType, subjectType);
unblock(c.resultType, constraint->location); unblock(c.resultType, constraint->location);
return true; return true;
@ -1391,7 +1415,7 @@ bool ConstraintSolver::tryDispatch(const SetPropConstraint& c, NotNull<const Con
{ {
LUAU_ASSERT(!subjectType->persistent); LUAU_ASSERT(!subjectType->persistent);
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, NotNull<const
if (tt->indexer) if (tt->indexer)
{ {
// TODO This probably has to be invariant. // 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<BoundType>(tt->indexer->indexResultType); asMutable(c.propType)->ty.emplace<BoundType>(tt->indexer->indexResultType);
asMutable(c.resultType)->ty.emplace<BoundType>(subjectType); asMutable(c.resultType)->ty.emplace<BoundType>(subjectType);
unblock(c.propType, constraint->location); unblock(c.propType, constraint->location);
@ -1435,7 +1459,7 @@ bool ConstraintSolver::tryDispatch(const SetIndexerConstraint& c, NotNull<const
else if (tt->state == TableState::Free || tt->state == TableState::Unsealed) else if (tt->state == TableState::Free || tt->state == TableState::Unsealed)
{ {
TypeId promotedIndexTy = freshType(arena, builtinTypes, tt->scope); TypeId promotedIndexTy = freshType(arena, builtinTypes, tt->scope);
unify(constraint->scope, constraint->location, c.indexType, promotedIndexTy); unify(constraint, c.indexType, promotedIndexTy);
auto mtt = getMutable<TableType>(subjectType); auto mtt = getMutable<TableType>(subjectType);
mtt->indexer = TableIndexer{promotedIndexTy, c.propType}; mtt->indexer = TableIndexer{promotedIndexTy, c.propType};
@ -1486,6 +1510,7 @@ bool ConstraintSolver::tryDispatch(const UnpackConstraint& c, NotNull<const Cons
if (isBlocked(resultPack)) if (isBlocked(resultPack))
{ {
LUAU_ASSERT(resultPack != sourcePack);
asMutable(resultPack)->ty.emplace<BoundTypePack>(sourcePack); asMutable(resultPack)->ty.emplace<BoundTypePack>(sourcePack);
unblock(resultPack, constraint->location); unblock(resultPack, constraint->location);
return true; return true;
@ -1525,7 +1550,7 @@ bool ConstraintSolver::tryDispatch(const UnpackConstraint& c, NotNull<const Cons
else else
{ {
LUAU_ASSERT(c.resultIsLValue); LUAU_ASSERT(c.resultIsLValue);
unify(constraint->scope, constraint->location, resultTy, srcTy); unify(constraint, resultTy, srcTy);
} }
unblock(resultTy, constraint->location); unblock(resultTy, constraint->location);
@ -1553,7 +1578,7 @@ bool ConstraintSolver::tryDispatch(const UnpackConstraint& c, NotNull<const Cons
apply(resultTy, srcTy); apply(resultTy, srcTy);
} }
else else
unify(constraint->scope, constraint->location, resultTy, srcTy); unify(constraint, resultTy, srcTy);
++resultIter; ++resultIter;
++i; ++i;
@ -1616,7 +1641,9 @@ bool ConstraintSolver::tryDispatch(const SetOpConstraint& c, NotNull<const Const
bool ConstraintSolver::tryDispatch(const ReduceConstraint& c, NotNull<const Constraint> constraint, bool force) bool ConstraintSolver::tryDispatch(const ReduceConstraint& c, NotNull<const Constraint> constraint, bool force)
{ {
TypeId ty = follow(c.ty); 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) for (TypeId r : result.reducedTypes)
unblock(r, constraint->location); unblock(r, constraint->location);
@ -1639,7 +1666,8 @@ bool ConstraintSolver::tryDispatch(const ReduceConstraint& c, NotNull<const Cons
bool ConstraintSolver::tryDispatch(const ReducePackConstraint& c, NotNull<const Constraint> constraint, bool force) bool ConstraintSolver::tryDispatch(const ReducePackConstraint& c, NotNull<const Constraint> constraint, bool force)
{ {
TypePackId tp = follow(c.tp); 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) for (TypeId r : result.reducedTypes)
unblock(r, constraint->location); unblock(r, constraint->location);
@ -1659,6 +1687,13 @@ bool ConstraintSolver::tryDispatch(const ReducePackConstraint& c, NotNull<const
return result.blockedTypes.empty() && result.blockedPacks.empty(); return result.blockedTypes.empty() && result.blockedPacks.empty();
} }
bool ConstraintSolver::tryDispatch(const EqualityConstraint& c, NotNull<const Constraint> 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<const Constraint> constraint, bool force) bool ConstraintSolver::tryDispatchIterableTable(TypeId iteratorTy, const IterableConstraint& c, NotNull<const Constraint> constraint, bool force)
{ {
auto block_ = [&](auto&& t) { auto block_ = [&](auto&& t) {
@ -1721,7 +1756,7 @@ bool ConstraintSolver::tryDispatchIterableTable(TypeId iteratorTy, const Iterabl
if (iteratorTable->indexer) if (iteratorTable->indexer)
{ {
TypePackId expectedVariablePack = arena->addTypePack({iteratorTable->indexer->indexType, iteratorTable->indexer->indexResultType}); 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); auto [variableTys, variablesTail] = flatten(c.variables);
@ -1755,7 +1790,7 @@ bool ConstraintSolver::tryDispatchIterableTable(TypeId iteratorTy, const Iterabl
if (auto iterFtv = get<FunctionType>(*instantiatedIterFn)) if (auto iterFtv = get<FunctionType>(*instantiatedIterFn))
{ {
TypePackId expectedIterArgs = arena->addTypePack({iteratorTy}); 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); 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 TypePackId nextRetPack = arena->addTypePack(TypePack{{firstIndex}, valueTailTy});
const TypeId expectedNextTy = arena->addType(FunctionType{nextArgPack, nextRetPack}); 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}); 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 TypePackId nextRetPack = arena->addTypePack(TypePack{{retIndex}, valueTailTy});
const TypeId expectedNextTy = arena->addType(FunctionType{TypeLevel{}, constraint->scope, nextArgPack, nextRetPack}); const TypeId expectedNextTy = arena->addType(FunctionType{TypeLevel{}, constraint->scope, nextArgPack, nextRetPack});
std::optional<TypeError> 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 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; (*c.astForInNextTypes)[c.nextAstFragment] = expectedNextTy;
else
reportError(*error);
auto it = begin(nextRetPack); auto it = begin(nextRetPack);
std::vector<TypeId> modifiedNextRetHead; std::vector<TypeId> modifiedNextRetHead;
@ -1882,14 +1915,14 @@ bool ConstraintSolver::tryDispatchIterableFunction(
} }
std::pair<std::vector<TypeId>, std::optional<TypeId>> ConstraintSolver::lookupTableProp( std::pair<std::vector<TypeId>, std::optional<TypeId>> ConstraintSolver::lookupTableProp(
TypeId subjectType, const std::string& propName, bool suppressSimplification) TypeId subjectType, const std::string& propName, ValueContext context, bool suppressSimplification)
{ {
DenseHashSet<TypeId> seen{nullptr}; DenseHashSet<TypeId> seen{nullptr};
return lookupTableProp(subjectType, propName, suppressSimplification, seen); return lookupTableProp(subjectType, propName, context, suppressSimplification, seen);
} }
std::pair<std::vector<TypeId>, std::optional<TypeId>> ConstraintSolver::lookupTableProp( std::pair<std::vector<TypeId>, std::optional<TypeId>> ConstraintSolver::lookupTableProp(
TypeId subjectType, const std::string& propName, bool suppressSimplification, DenseHashSet<TypeId>& seen) TypeId subjectType, const std::string& propName, ValueContext context, bool suppressSimplification, DenseHashSet<TypeId>& seen)
{ {
if (seen.contains(subjectType)) if (seen.contains(subjectType))
return {}; return {};
@ -1906,19 +1939,58 @@ std::pair<std::vector<TypeId>, std::optional<TypeId>> ConstraintSolver::lookupTa
else if (auto ttv = getMutable<TableType>(subjectType)) else if (auto ttv = getMutable<TableType>(subjectType))
{ {
if (auto prop = ttv->props.find(propName); prop != ttv->props.end()) 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}; return {{}, ttv->indexer->indexResultType};
else if (ttv->state == TableState::Free)
if (ttv->state == TableState::Free)
{ {
TypeId result = freshType(arena, builtinTypes, ttv->scope); 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}; return {{}, result};
} }
} }
else if (auto mt = get<MetatableType>(subjectType)) else if (auto mt = get<MetatableType>(subjectType))
{ {
auto [blocked, result] = lookupTableProp(mt->table, propName, suppressSimplification, seen); auto [blocked, result] = lookupTableProp(mt->table, propName, context, suppressSimplification, seen);
if (!blocked.empty() || result) if (!blocked.empty() || result)
return {blocked, result}; return {blocked, result};
@ -1949,13 +2021,13 @@ std::pair<std::vector<TypeId>, std::optional<TypeId>> ConstraintSolver::lookupTa
} }
} }
else else
return lookupTableProp(indexType, propName, suppressSimplification, seen); return lookupTableProp(indexType, propName, context, suppressSimplification, seen);
} }
} }
else if (auto ct = get<ClassType>(subjectType)) else if (auto ct = get<ClassType>(subjectType))
{ {
if (auto p = lookupClassProp(ct, propName)) if (auto p = lookupClassProp(ct, propName))
return {{}, p->type()}; return {{}, context == ValueContext::RValue ? p->readTy : p->writeTy};
if (ct->indexer) if (ct->indexer)
{ {
return {{}, ct->indexer->indexResultType}; return {{}, ct->indexer->indexResultType};
@ -1970,14 +2042,14 @@ std::pair<std::vector<TypeId>, std::optional<TypeId>> ConstraintSolver::lookupTa
if (indexProp == metatable->props.end()) if (indexProp == metatable->props.end())
return {{}, std::nullopt}; 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<FreeType>(subjectType)) else if (auto ft = get<FreeType>(subjectType))
{ {
const TypeId upperBound = follow(ft->upperBound); const TypeId upperBound = follow(ft->upperBound);
if (get<TableType>(upperBound) || get<PrimitiveType>(upperBound)) if (get<TableType>(upperBound) || get<PrimitiveType>(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. // TODO: The upper bound could be an intersection that contains suitable tables or classes.
@ -1987,7 +2059,16 @@ std::pair<std::vector<TypeId>, std::optional<TypeId>> ConstraintSolver::lookupTa
TableType* tt = getMutable<TableType>(newUpperBound); TableType* tt = getMutable<TableType>(newUpperBound);
LUAU_ASSERT(tt); LUAU_ASSERT(tt);
TypeId propType = freshType(arena, builtinTypes, scope); 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); unify(scope, Location{}, subjectType, newUpperBound);
@ -2000,7 +2081,7 @@ std::pair<std::vector<TypeId>, std::optional<TypeId>> ConstraintSolver::lookupTa
for (TypeId ty : utv) 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()); blocked.insert(blocked.end(), innerBlocked.begin(), innerBlocked.end());
if (innerResult) if (innerResult)
options.insert(*innerResult); options.insert(*innerResult);
@ -2029,7 +2110,7 @@ std::pair<std::vector<TypeId>, std::optional<TypeId>> ConstraintSolver::lookupTa
for (TypeId ty : itv) 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()); blocked.insert(blocked.end(), innerBlocked.begin(), innerBlocked.end());
if (innerResult) if (innerResult)
options.insert(*innerResult); options.insert(*innerResult);
@ -2055,34 +2136,39 @@ std::pair<std::vector<TypeId>, std::optional<TypeId>> ConstraintSolver::lookupTa
return {{}, std::nullopt}; return {{}, std::nullopt};
} }
template<typename TID> template <typename TID>
bool ConstraintSolver::tryUnify(NotNull<const Constraint> constraint, TID subTy, TID superTy) bool ConstraintSolver::unify(NotNull<Scope> 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 (const auto& [expanded, additions] : u2.expandedFreeTypes)
{ {
for (TypeId addition : additions) for (TypeId addition : additions)
upperBoundContributors[expanded].push_back(std::make_pair(constraint->location, addition)); upperBoundContributors[expanded].push_back(std::make_pair(location, addition));
} }
} }
else else
{ {
// Unification only fails when doing so would fail the occurs check. reportError(OccursCheckFailed{}, location);
// ie create a self-bound type or a cyclic type pack return false;
reportError(OccursCheckFailed{}, constraint->location);
} }
unblock(subTy, constraint->location); unblock(subType, location);
unblock(superTy, constraint->location); unblock(superType, location);
return true; return true;
} }
template<typename TID>
bool ConstraintSolver::unify(NotNull<const Constraint> constraint, TID subTy, TID superTy)
{
return unify(constraint->scope, constraint->location, subTy, superTy);
}
void ConstraintSolver::bindBlockedType(TypeId blockedTy, TypeId resultTy, TypeId rootTy, Location location) void ConstraintSolver::bindBlockedType(TypeId blockedTy, TypeId resultTy, TypeId rootTy, Location location)
{ {
resultTy = follow(resultTy); resultTy = follow(resultTy);
@ -2297,6 +2383,21 @@ void ConstraintSolver::unblock(const std::vector<TypePackId>& packs, Location lo
unblock(t, location); unblock(t, location);
} }
void ConstraintSolver::reproduceConstraints(NotNull<Scope> scope, const Location& location, const Substitution& subst)
{
for (auto [_, newTy] : subst.newTypes)
{
if (get<TypeFamilyInstanceType>(newTy))
pushConstraint(scope, location, ReduceConstraint{newTy});
}
for (auto [_, newPack] : subst.newPacks)
{
if (get<TypeFamilyInstanceTypePack>(newPack))
pushConstraint(scope, location, ReducePackConstraint{newPack});
}
}
bool ConstraintSolver::isBlocked(TypeId ty) bool ConstraintSolver::isBlocked(TypeId ty)
{ {
ty = follow(ty); ty = follow(ty);
@ -2318,39 +2419,6 @@ bool ConstraintSolver::isBlocked(NotNull<const Constraint> constraint)
return blockedIt != blockedConstraints.end() && blockedIt->second > 0; return blockedIt != blockedConstraints.end() && blockedIt->second > 0;
} }
std::optional<TypeError> ConstraintSolver::unify(NotNull<Scope> 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> 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<Constraint> ConstraintSolver::pushConstraint(NotNull<Scope> scope, const Location& location, ConstraintV cv) NotNull<Constraint> ConstraintSolver::pushConstraint(NotNull<Scope> scope, const Location& location, ConstraintV cv)
{ {
std::unique_ptr<Constraint> c = std::make_unique<Constraint>(scope, location, std::move(cv)); std::unique_ptr<Constraint> c = std::make_unique<Constraint>(scope, location, std::move(cv));

View file

@ -541,11 +541,36 @@ struct ErrorConverter
"' is used in a way that will run time error"; "' 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 "<Invalid PropertyAccessViolation>";
}
std::string operator()(const CheckedFunctionIncorrectArgs& e) const std::string operator()(const CheckedFunctionIncorrectArgs& e) const
{ {
return "Checked Function " + e.functionName + " expects " + std::to_string(e.expected) + " arguments, but received " + return "Checked Function " + e.functionName + " expects " + std::to_string(e.expected) + " arguments, but received " +
std::to_string(e.actual); 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 struct InvalidNameChecker
@ -638,6 +663,11 @@ bool UnknownProperty::operator==(const UnknownProperty& rhs) const
return *table == *rhs.table && key == rhs.key; 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 bool NotATable::operator==(const NotATable& rhs) const
{ {
return ty == rhs.ty; return ty == rhs.ty;
@ -884,6 +914,16 @@ bool CheckedFunctionIncorrectArgs::operator==(const CheckedFunctionIncorrectArgs
return functionName == rhs.functionName && expected == rhs.expected && actual == rhs.actual; 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) std::string toString(const TypeError& error)
{ {
return toString(error, TypeErrorToStringOptions{}); return toString(error, TypeErrorToStringOptions{});
@ -1059,9 +1099,15 @@ void copyError(T& e, TypeArena& destArena, CloneState& cloneState)
{ {
e.argumentType = clone(e.argumentType); e.argumentType = clone(e.argumentType);
} }
else if constexpr (std::is_same_v<T, PropertyAccessViolation>)
e.table = clone(e.table);
else if constexpr (std::is_same_v<T, CheckedFunctionIncorrectArgs>) else if constexpr (std::is_same_v<T, CheckedFunctionIncorrectArgs>)
{ {
} }
else if constexpr (std::is_same_v<T, UnexpectedTypeInSubtyping>)
e.ty = clone(e.ty);
else if constexpr (std::is_same_v<T, UnexpectedTypePackInSubtyping>)
e.tp = clone(e.tp);
else else
static_assert(always_false_v<T>, "Non-exhaustive type switch"); static_assert(always_false_v<T>, "Non-exhaustive type switch");
} }

View file

@ -32,9 +32,8 @@ LUAU_FASTINT(LuauTypeInferRecursionLimit)
LUAU_FASTINT(LuauTarjanChildLimit) LUAU_FASTINT(LuauTarjanChildLimit)
LUAU_FASTFLAG(LuauInferInNoCheckMode) LUAU_FASTFLAG(LuauInferInNoCheckMode)
LUAU_FASTFLAGVARIABLE(LuauKnowsTheDataModel3, false) LUAU_FASTFLAGVARIABLE(LuauKnowsTheDataModel3, false)
LUAU_FASTFLAGVARIABLE(DebugLuauDeferredConstraintResolution, false) LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution)
LUAU_FASTFLAGVARIABLE(DebugLuauLogSolverToJson, false) LUAU_FASTFLAGVARIABLE(DebugLuauLogSolverToJson, false)
LUAU_FASTFLAGVARIABLE(DebugLuauReadWriteProperties, false)
LUAU_FASTFLAGVARIABLE(DebugLuauLogSolverToJsonFile, false) LUAU_FASTFLAGVARIABLE(DebugLuauLogSolverToJsonFile, false)
namespace Luau namespace Luau
@ -1219,6 +1218,15 @@ ModulePtr check(const SourceModule& sourceModule, Mode mode, const std::vector<R
result->cancelled = true; result->cancelled = 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) for (TypeError& e : cs.errors)
result->errors.emplace_back(std::move(e)); result->errors.emplace_back(std::move(e));
@ -1263,15 +1271,6 @@ ModulePtr check(const SourceModule& sourceModule, Mode mode, const std::vector<R
freeze(result->internalTypes); freeze(result->internalTypes);
freeze(result->interfaceTypes); 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; return result;
} }

View file

@ -2,6 +2,7 @@
#include "Luau/Instantiation.h" #include "Luau/Instantiation.h"
#include "Luau/Common.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/ToString.h"
#include "Luau/TxnLog.h" #include "Luau/TxnLog.h"
#include "Luau/TypeArena.h" #include "Luau/TypeArena.h"
@ -143,39 +144,6 @@ TypePackId ReplaceGenerics::clean(TypePackId tp)
return addTypePack(TypePackVar(FreeTypePack{scope, level})); return addTypePack(TypePackVar(FreeTypePack{scope, level}));
} }
struct Replacer : Substitution
{
DenseHashMap<TypeId, TypeId> replacements;
DenseHashMap<TypePackId, TypePackId> replacementPacks;
Replacer(NotNull<TypeArena> arena, DenseHashMap<TypeId, TypeId> replacements, DenseHashMap<TypePackId, TypePackId> 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<TypeId> instantiate( std::optional<TypeId> instantiate(
NotNull<BuiltinTypes> builtinTypes, NotNull<TypeArena> arena, NotNull<TypeCheckLimits> limits, NotNull<Scope> scope, TypeId ty) NotNull<BuiltinTypes> builtinTypes, NotNull<TypeArena> arena, NotNull<TypeCheckLimits> limits, NotNull<Scope> scope, TypeId ty)
{ {

View file

@ -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<GenericType>(ty) && genericSubstitutions.contains(ty);
}
bool Instantiation2::isDirty(TypePackId tp)
{
return get<GenericTypePack>(tp) && genericPackSubstitutions.contains(tp);
}
TypeId Instantiation2::clean(TypeId ty)
{
TypeId substTy = genericSubstitutions[ty];
const FreeType* ft = get<FreeType>(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<NeverType>(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

View file

@ -207,9 +207,15 @@ static void errorToString(std::ostream& stream, const T& err)
else if constexpr (std::is_same_v<T, NonStrictFunctionDefinitionError>) else if constexpr (std::is_same_v<T, NonStrictFunctionDefinitionError>)
stream << "NonStrictFunctionDefinitionError { functionName = '" + err.functionName + "', argument = '" + err.argument + stream << "NonStrictFunctionDefinitionError { functionName = '" + err.functionName + "', argument = '" + err.argument +
"', argumentType = '" + toString(err.argumentType) + "' }"; "', argumentType = '" + toString(err.argumentType) + "' }";
else if constexpr (std::is_same_v<T, PropertyAccessViolation>)
stream << "PropertyAccessViolation { table = " << toString(err.table) << ", prop = '" << err.key << "', context = " << err.context << " }";
else if constexpr (std::is_same_v<T, CheckedFunctionIncorrectArgs>) else if constexpr (std::is_same_v<T, CheckedFunctionIncorrectArgs>)
stream << "CheckedFunction { functionName = '" + err.functionName + ", expected = " + std::to_string(err.expected) + stream << "CheckedFunction { functionName = '" + err.functionName + ", expected = " + std::to_string(err.expected) +
", actual = " + std::to_string(err.actual) + "}"; ", actual = " + std::to_string(err.actual) + "}";
else if constexpr (std::is_same_v<T, UnexpectedTypeInSubtyping>)
stream << "UnexpectedTypeInSubtyping { ty = '" + toString(err.ty) + "' }";
else if constexpr (std::is_same_v<T, UnexpectedTypePackInSubtyping>)
stream << "UnexpectedTypePackInSubtyping { tp = '" + toString(err.tp) + "' }";
else else
static_assert(always_false_v<T>, "Non-exhaustive type switch"); static_assert(always_false_v<T>, "Non-exhaustive type switch");
} }

View file

@ -14,6 +14,8 @@
LUAU_FASTINTVARIABLE(LuauSuggestionDistance, 4) LUAU_FASTINTVARIABLE(LuauSuggestionDistance, 4)
LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution)
namespace Luau namespace Luau
{ {
@ -1848,6 +1850,49 @@ private:
bool visit(AstTypeTable* node) override bool visit(AstTypeTable* node) override
{ {
if (FFlag::DebugLuauDeferredConstraintResolution)
{
struct Rec
{
AstTableAccess access;
Location location;
};
DenseHashMap<AstName, Rec> 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<AstName, int> names(AstName{}); DenseHashMap<AstName, int> names(AstName{});
for (const AstTableProp& item : node->props) for (const AstTableProp& item : node->props)

View file

@ -9,6 +9,7 @@
#include "Luau/Common.h" #include "Luau/Common.h"
#include "Luau/RecursionCounter.h" #include "Luau/RecursionCounter.h"
#include "Luau/Set.h" #include "Luau/Set.h"
#include "Luau/Simplify.h"
#include "Luau/Subtyping.h" #include "Luau/Subtyping.h"
#include "Luau/Type.h" #include "Luau/Type.h"
#include "Luau/TypeFwd.h" #include "Luau/TypeFwd.h"
@ -20,7 +21,6 @@ LUAU_FASTFLAGVARIABLE(DebugLuauCheckNormalizeInvariant, false)
LUAU_FASTINTVARIABLE(LuauNormalizeIterationLimit, 1200); LUAU_FASTINTVARIABLE(LuauNormalizeIterationLimit, 1200);
LUAU_FASTINTVARIABLE(LuauNormalizeCacheLimit, 100000); LUAU_FASTINTVARIABLE(LuauNormalizeCacheLimit, 100000);
LUAU_FASTFLAG(LuauTransitiveSubtyping) LUAU_FASTFLAG(LuauTransitiveSubtyping)
LUAU_FASTFLAG(DebugLuauReadWriteProperties)
LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution) LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution)
namespace Luau namespace Luau
@ -472,11 +472,11 @@ bool Normalizer::isInhabited(TypeId ty, Set<TypeId>& seen)
{ {
for (const auto& [_, prop] : ttv->props) 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, // 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. // 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; return false;
} }
else else
@ -2349,12 +2349,61 @@ std::optional<TypeId> Normalizer::intersectionOfTables(TypeId here, TypeId there
{ {
const auto& [_name, tprop] = *tfound; const auto& [_name, tprop] = *tfound;
// TODO: variance issues here, which can't be fixed until we have read/write property types // TODO: variance issues here, which can't be fixed until we have read/write property types
prop.setType(intersectionType(hprop.type(), tprop.type())); if (FFlag::DebugLuauDeferredConstraintResolution)
hereSubThere &= (prop.type() == hprop.type()); {
thereSubHere &= (prop.type() == tprop.type()); 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 // TODO: string indexers
result.props[name] = prop;
if (prop.readTy || prop.writeTy)
result.props[name] = prop;
} }
for (const auto& [name, tprop] : tttv->props) for (const auto& [name, tprop] : tttv->props)
@ -2431,8 +2480,10 @@ void Normalizer::intersectTablesWithTable(TypeIds& heres, TypeId there)
{ {
TypeIds tmp; TypeIds tmp;
for (TypeId here : heres) for (TypeId here : heres)
{
if (std::optional<TypeId> inter = intersectionOfTables(here, there)) if (std::optional<TypeId> inter = intersectionOfTables(here, there))
tmp.insert(*inter); tmp.insert(*inter);
}
heres.retain(tmp); heres.retain(tmp);
heres.insert(tmp.begin(), tmp.end()); heres.insert(tmp.begin(), tmp.end());
} }
@ -2441,9 +2492,14 @@ void Normalizer::intersectTables(TypeIds& heres, const TypeIds& theres)
{ {
TypeIds tmp; TypeIds tmp;
for (TypeId here : heres) for (TypeId here : heres)
{
for (TypeId there : theres) for (TypeId there : theres)
{
if (std::optional<TypeId> inter = intersectionOfTables(here, there)) if (std::optional<TypeId> inter = intersectionOfTables(here, there))
tmp.insert(*inter); tmp.insert(*inter);
}
}
heres.retain(tmp); heres.retain(tmp);
heres.insert(tmp.begin(), tmp.end()); heres.insert(tmp.begin(), tmp.end());
} }

View file

@ -3,10 +3,10 @@
#include "Luau/Simplify.h" #include "Luau/Simplify.h"
#include "Luau/DenseHash.h" #include "Luau/DenseHash.h"
#include "Luau/Normalize.h" // TypeIds
#include "Luau/RecursionCounter.h" #include "Luau/RecursionCounter.h"
#include "Luau/ToString.h" #include "Luau/Set.h"
#include "Luau/TypeArena.h" #include "Luau/TypeArena.h"
#include "Luau/TypePairHash.h"
#include "Luau/TypeUtils.h" #include "Luau/TypeUtils.h"
#include <algorithm> #include <algorithm>
@ -17,6 +17,8 @@ LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution)
namespace Luau namespace Luau
{ {
using SimplifierSeenSet = Set<std::pair<TypeId, TypeId>, TypePairHash>;
struct TypeSimplifier struct TypeSimplifier
{ {
NotNull<BuiltinTypes> builtinTypes; NotNull<BuiltinTypes> builtinTypes;
@ -226,23 +228,27 @@ static bool isTypeVariable(TypeId ty)
return get<FreeType>(ty) || get<GenericType>(ty) || get<BlockedType>(ty) || get<PendingExpansionType>(ty); return get<FreeType>(ty) || get<GenericType>(ty) || get<BlockedType>(ty) || get<PendingExpansionType>(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<const TableType> leftTable{get<TableType>(left)}; NotNull<const TableType> leftTable{get<TableType>(left)};
NotNull<const TableType> rightTable{get<TableType>(right)}; NotNull<const TableType> rightTable{get<TableType>(right)};
LUAU_ASSERT(1 == rightTable->props.size()); LUAU_ASSERT(1 == rightTable->props.size());
// Disjoint props have nothing in common // 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 // 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) { bool foundPropFromLeftInRight = std::any_of(begin(leftTable->props), end(leftTable->props),
return rightTable->props.find(prop.first) != end(rightTable->props); [&](auto prop)
}); {
bool foundPropFromRightInLeft = std::any_of(begin(rightTable->props), end(rightTable->props), [&](auto prop) { return rightTable->props.count(prop.first) > 0;
return leftTable->props.find(prop.first) != end(leftTable->props); });
}); bool foundPropFromRightInLeft = std::any_of(begin(rightTable->props), end(rightTable->props),
[&](auto prop)
{
return leftTable->props.count(prop.first) > 0;
});
if (!(foundPropFromLeftInRight || foundPropFromRightInLeft) && leftTable->props.size() >= 1 && rightTable->props.size() >= 1) if (!foundPropFromLeftInRight && !foundPropFromRightInLeft && leftTable->props.size() >= 1 && rightTable->props.size() >= 1)
return Relation::Disjoint; return Relation::Disjoint;
const auto [propName, rightProp] = *begin(rightTable->props); const auto [propName, rightProp] = *begin(rightTable->props);
@ -257,7 +263,10 @@ Relation relateTables(TypeId left, TypeId right)
const Property leftProp = it->second; 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()) if (r == Relation::Coincident && 1 != leftTable->props.size())
{ {
// eg {tag: "cat", prop: string} & {tag: "cat"} // eg {tag: "cat", prop: string} & {tag: "cat"}
@ -268,7 +277,7 @@ Relation relateTables(TypeId left, TypeId right)
} }
// A cheap and approximate subtype test // 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 // 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) if (left == right)
return Relation::Coincident; return Relation::Coincident;
std::pair<TypeId, TypeId> 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<UnknownType>(left)) if (get<UnknownType>(left))
{ {
if (get<AnyType>(right)) if (get<AnyType>(right))
@ -291,7 +308,7 @@ Relation relate(TypeId left, TypeId right)
} }
if (get<UnknownType>(right)) if (get<UnknownType>(right))
return flip(relate(right, left)); return flip(relate(right, left, seen));
if (get<AnyType>(left)) if (get<AnyType>(left))
{ {
@ -302,7 +319,7 @@ Relation relate(TypeId left, TypeId right)
} }
if (get<AnyType>(right)) if (get<AnyType>(right))
return flip(relate(right, left)); return flip(relate(right, left, seen));
// Type variables // Type variables
// * FreeType // * FreeType
@ -340,7 +357,7 @@ Relation relate(TypeId left, TypeId right)
return Relation::Disjoint; return Relation::Disjoint;
} }
if (get<ErrorType>(right)) if (get<ErrorType>(right))
return flip(relate(right, left)); return flip(relate(right, left, seen));
if (get<NeverType>(left)) if (get<NeverType>(left))
{ {
@ -350,7 +367,7 @@ Relation relate(TypeId left, TypeId right)
return Relation::Subset; return Relation::Subset;
} }
if (get<NeverType>(right)) if (get<NeverType>(right))
return flip(relate(right, left)); return flip(relate(right, left, seen));
if (auto ut = get<IntersectionType>(left)) if (auto ut = get<IntersectionType>(left))
return Relation::Intersects; return Relation::Intersects;
@ -363,14 +380,14 @@ Relation relate(TypeId left, TypeId right)
{ {
std::vector<Relation> opts; std::vector<Relation> opts;
for (TypeId part : ut) for (TypeId part : ut)
if (relate(left, part) == Relation::Subset) if (relate(left, part, seen) == Relation::Subset)
return Relation::Subset; return Relation::Subset;
return Relation::Intersects; return Relation::Intersects;
} }
if (auto rnt = get<NegationType>(right)) if (auto rnt = get<NegationType>(right))
{ {
Relation a = relate(left, rnt->ty); Relation a = relate(left, rnt->ty, seen);
switch (a) switch (a)
{ {
case Relation::Coincident: case Relation::Coincident:
@ -401,7 +418,7 @@ Relation relate(TypeId left, TypeId right)
} }
} }
else if (get<NegationType>(left)) else if (get<NegationType>(left))
return flip(relate(right, left)); return flip(relate(right, left, seen));
if (auto lp = get<PrimitiveType>(left)) if (auto lp = get<PrimitiveType>(left))
{ {
@ -448,7 +465,7 @@ Relation relate(TypeId left, TypeId right)
return Relation::Disjoint; return Relation::Disjoint;
if (get<PrimitiveType>(right)) if (get<PrimitiveType>(right))
return flip(relate(right, left)); return flip(relate(right, left, seen));
if (auto rs = get<SingletonType>(right)) if (auto rs = get<SingletonType>(right))
{ {
if (ls->variant == rs->variant) if (ls->variant == rs->variant)
@ -485,7 +502,7 @@ Relation relate(TypeId left, TypeId right)
// TODO PROBABLY indexers and metatables. // TODO PROBABLY indexers and metatables.
if (1 == rt->props.size()) 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 * A reduction of these intersections is certainly possible, but
* it would require minting new table types. Also, I don't think * it would require minting new table types. Also, I don't think
@ -504,7 +521,7 @@ Relation relate(TypeId left, TypeId right)
return r; return r;
} }
else if (1 == lt->props.size()) else if (1 == lt->props.size())
return flip(relate(right, left)); return flip(relate(right, left, seen));
else else
return Relation::Intersects; return Relation::Intersects;
} }
@ -531,6 +548,13 @@ Relation relate(TypeId left, TypeId right)
return Relation::Intersects; 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 TypeSimplifier::mkNegation(TypeId ty)
{ {
TypeId result = nullptr; TypeId result = nullptr;
@ -1056,7 +1080,7 @@ std::optional<TypeId> TypeSimplifier::basicIntersect(TypeId left, TypeId right)
const auto [propName, leftProp] = *begin(lt->props); const auto [propName, leftProp] = *begin(lt->props);
auto it = rt->props.find(propName); 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()); Relation r = relate(leftProp.type(), it->second.type());
@ -1266,9 +1290,12 @@ TypeId TypeSimplifier::simplify(TypeId ty, DenseHashSet<TypeId>& seen)
{ {
if (1 == tt->props.size()) if (1 == tt->props.size())
{ {
TypeId propTy = simplify(begin(tt->props)->second.type(), seen); if (std::optional<TypeId> readTy = begin(tt->props)->second.readTy)
if (get<NeverType>(propTy)) {
return builtinTypes->neverType; TypeId propTy = simplify(*readTy, seen);
if (get<NeverType>(propTy))
return builtinTypes->neverType;
}
} }
} }

View file

@ -9,7 +9,7 @@
#include <stdexcept> #include <stdexcept>
LUAU_FASTINTVARIABLE(LuauTarjanChildLimit, 10000) LUAU_FASTINTVARIABLE(LuauTarjanChildLimit, 10000)
LUAU_FASTFLAG(DebugLuauReadWriteProperties) LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution);
LUAU_FASTFLAGVARIABLE(LuauPreallocateTarjanVectors, false); LUAU_FASTFLAGVARIABLE(LuauPreallocateTarjanVectors, false);
LUAU_FASTINTVARIABLE(LuauTarjanPreallocationSize, 256); LUAU_FASTINTVARIABLE(LuauTarjanPreallocationSize, 256);
@ -185,10 +185,10 @@ void Tarjan::visitChildren(TypeId ty, int index)
LUAU_ASSERT(!ttv->boundTo); LUAU_ASSERT(!ttv->boundTo);
for (const auto& [name, prop] : ttv->props) for (const auto& [name, prop] : ttv->props)
{ {
if (FFlag::DebugLuauReadWriteProperties) if (FFlag::DebugLuauDeferredConstraintResolution)
{ {
visitChild(prop.readType()); visitChild(prop.readTy);
visitChild(prop.writeType()); visitChild(prop.writeTy);
} }
else else
visitChild(prop.type()); visitChild(prop.type());
@ -700,8 +700,8 @@ void Substitution::replaceChildren(TypeId ty)
LUAU_ASSERT(!ttv->boundTo); LUAU_ASSERT(!ttv->boundTo);
for (auto& [name, prop] : ttv->props) for (auto& [name, prop] : ttv->props)
{ {
if (FFlag::DebugLuauReadWriteProperties) if (FFlag::DebugLuauDeferredConstraintResolution)
prop = Property::create(replace(prop.readType()), replace(prop.writeType())); prop = Property::create(replace(prop.readTy), replace(prop.writeTy));
else else
prop.setType(replace(prop.type())); prop.setType(replace(prop.type()));
} }

View file

@ -7,7 +7,9 @@
#include "Luau/Normalize.h" #include "Luau/Normalize.h"
#include "Luau/Scope.h" #include "Luau/Scope.h"
#include "Luau/StringUtils.h" #include "Luau/StringUtils.h"
#include "Luau/Substitution.h"
#include "Luau/ToString.h" #include "Luau/ToString.h"
#include "Luau/TxnLog.h"
#include "Luau/Type.h" #include "Luau/Type.h"
#include "Luau/TypeArena.h" #include "Luau/TypeArena.h"
#include "Luau/TypeCheckLimits.h" #include "Luau/TypeCheckLimits.h"
@ -217,7 +219,14 @@ SubtypingResult& SubtypingResult::withSuperPath(TypePath::Path path)
SubtypingResult& SubtypingResult::withErrors(ErrorVec& err) 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; return *this;
} }
@ -245,6 +254,74 @@ SubtypingResult SubtypingResult::any(const std::vector<SubtypingResult>& results
return acc; return acc;
} }
struct ApplyMappedGenerics : Substitution
{
using MappedGenerics = DenseHashMap<TypeId, SubtypingEnvironment::GenericBounds>;
using MappedGenericPacks = DenseHashMap<TypePackId, TypePackId>;
NotNull<BuiltinTypes> builtinTypes;
NotNull<TypeArena> arena;
MappedGenerics& mappedGenerics;
MappedGenericPacks& mappedGenericPacks;
ApplyMappedGenerics(NotNull<BuiltinTypes> builtinTypes, NotNull<TypeArena> 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<TypeId>(begin(bounds.upperBound), end(bounds.upperBound))});
}
TypePackId clean(TypePackId tp) override
{
return mappedGenericPacks[tp];
}
bool ignoreChildren(TypeId ty) override
{
if (get<ClassType>(ty))
return true;
return ty->persistent;
}
bool ignoreChildren(TypePackId ty) override
{
return ty->persistent;
}
};
std::optional<TypeId> SubtypingEnvironment::applyMappedGenerics(NotNull<BuiltinTypes> builtinTypes, NotNull<TypeArena> arena, TypeId ty)
{
ApplyMappedGenerics amg{builtinTypes, arena, mappedGenerics, mappedGenericPacks};
return amg.substitute(ty);
}
Subtyping::Subtyping(NotNull<BuiltinTypes> builtinTypes, NotNull<TypeArena> typeArena, NotNull<Normalizer> normalizer, Subtyping::Subtyping(NotNull<BuiltinTypes> builtinTypes, NotNull<TypeArena> typeArena, NotNull<Normalizer> normalizer,
NotNull<InternalErrorReporter> iceReporter, NotNull<Scope> scope) NotNull<InternalErrorReporter> iceReporter, NotNull<Scope> scope)
: builtinTypes(builtinTypes) : builtinTypes(builtinTypes)
@ -493,9 +570,19 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypeId sub
} }
} }
else if (auto subTypeFamilyInstance = get<TypeFamilyInstanceType>(subTy)) else if (auto subTypeFamilyInstance = get<TypeFamilyInstanceType>(subTy))
{
if (auto substSubTy = env.applyMappedGenerics(builtinTypes, arena, subTy))
subTypeFamilyInstance = get<TypeFamilyInstanceType>(*substSubTy);
result = isCovariantWith(env, subTypeFamilyInstance, superTy); result = isCovariantWith(env, subTypeFamilyInstance, superTy);
}
else if (auto superTypeFamilyInstance = get<TypeFamilyInstanceType>(superTy)) else if (auto superTypeFamilyInstance = get<TypeFamilyInstanceType>(superTy))
{
if (auto substSuperTy = env.applyMappedGenerics(builtinTypes, arena, superTy))
superTypeFamilyInstance = get<TypeFamilyInstanceType>(*substSuperTy);
result = isCovariantWith(env, subTy, superTypeFamilyInstance); result = isCovariantWith(env, subTy, superTypeFamilyInstance);
}
else if (auto subGeneric = get<GenericType>(subTy); subGeneric && variance == Variance::Covariant) else if (auto subGeneric = get<GenericType>(subTy); subGeneric && variance == Variance::Covariant)
{ {
bool ok = bindGeneric(env, subTy, superTy); bool ok = bindGeneric(env, subTy, superTy);
@ -604,7 +691,9 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypePackId
else if (get<ErrorTypePack>(*subTail)) else if (get<ErrorTypePack>(*subTail))
return SubtypingResult{true}.withSubComponent(TypePath::PackField::Tail); return SubtypingResult{true}.withSubComponent(TypePath::PackField::Tail);
else else
unexpected(*subTail); return SubtypingResult{false}
.withSubComponent(TypePath::PackField::Tail)
.withError({scope->location, UnexpectedTypePackInSubtyping{*subTail}});
} }
else else
{ {
@ -656,7 +745,9 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypePackId
else if (get<ErrorTypePack>(*superTail)) else if (get<ErrorTypePack>(*superTail))
return SubtypingResult{true}.withSuperComponent(TypePath::PackField::Tail); return SubtypingResult{true}.withSuperComponent(TypePath::PackField::Tail);
else else
unexpected(*superTail); return SubtypingResult{false}
.withSuperComponent(TypePath::PackField::Tail)
.withError({scope->location, UnexpectedTypePackInSubtyping{*subTail}});
} }
else else
return {false}; return {false};
@ -717,8 +808,10 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypePackId
// error type is fine on either side // error type is fine on either side
results.push_back(SubtypingResult{true}.withBothComponent(TypePath::PackField::Tail)); results.push_back(SubtypingResult{true}.withBothComponent(TypePath::PackField::Tail));
else else
iceReporter->ice( return SubtypingResult{false}
format("Subtyping::isSubtype got unexpected type packs %s and %s", toString(*subTail).c_str(), toString(*superTail).c_str())); .withBothComponent(TypePath::PackField::Tail)
.withError({scope->location, UnexpectedTypePackInSubtyping{*subTail}})
.withError({scope->location, UnexpectedTypePackInSubtyping{*superTail}});
} }
else if (subTail) else if (subTail)
{ {
@ -732,7 +825,9 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypePackId
return SubtypingResult{ok}.withSubComponent(TypePath::PackField::Tail); return SubtypingResult{ok}.withSubComponent(TypePath::PackField::Tail);
} }
else else
unexpected(*subTail); return SubtypingResult{false}
.withSubComponent(TypePath::PackField::Tail)
.withError({scope->location, UnexpectedTypePackInSubtyping{*subTail}});
} }
else if (superTail) else if (superTail)
{ {
@ -759,7 +854,9 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypePackId
results.push_back(SubtypingResult{false}.withSuperComponent(TypePath::PackField::Tail)); results.push_back(SubtypingResult{false}.withSuperComponent(TypePath::PackField::Tail));
} }
else 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); SubtypingResult result = SubtypingResult::all(results);
@ -1126,18 +1223,32 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const Tabl
if (subTable->props.empty() && !subTable->indexer && superTable->indexer) if (subTable->props.empty() && !subTable->indexer && superTable->indexer)
return {false}; return {false};
for (const auto& [name, prop] : superTable->props) for (const auto& [name, superProp] : superTable->props)
{ {
std::vector<SubtypingResult> results; std::vector<SubtypingResult> results;
if (auto it = subTable->props.find(name); it != subTable->props.end()) if (auto subIter = subTable->props.find(name); subIter != subTable->props.end())
results.push_back(isInvariantWith(env, it->second.type(), prop.type()).withBothComponent(TypePath::Property(name))); results.push_back(isCovariantWith(env, subIter->second, superProp, name));
if (subTable->indexer) if (subTable->indexer)
{ {
if (isInvariantWith(env, subTable->indexer->indexType, builtinTypes->stringType).isSubtype) if (isCovariantWith(env, builtinTypes->stringType, subTable->indexer->indexType).isSubtype)
results.push_back(isInvariantWith(env, subTable->indexer->indexResultType, prop.type()) {
.withSubComponent(TypePath::TypeField::IndexResult) if (superProp.isShared())
.withSuperComponent(TypePath::Property(name))); 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()) if (results.empty())
@ -1197,7 +1308,9 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const Clas
for (const auto& [name, prop] : superTable->props) for (const auto& [name, prop] : superTable->props)
{ {
if (auto classProp = lookupClassProp(subClass, name)) 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 else
return SubtypingResult{false}; return SubtypingResult{false};
} }
@ -1230,7 +1343,7 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const Prim
{ {
if (auto stringTable = get<TableType>(it->second.type())) if (auto stringTable = get<TableType>(it->second.type()))
result.orElse( 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<TableType>(it->second.type())) if (auto stringTable = get<TableType>(it->second.type()))
result.orElse( 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)); .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) SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const NormalizedType* subNorm, const NormalizedType* superNorm)
{ {
if (!subNorm || !superNorm) if (!subNorm || !superNorm)
@ -1473,14 +1611,4 @@ TypeId Subtyping::makeAggregateType(const Container& container, TypeId orElse)
return arena->addType(T{std::vector<TypeId>(begin(container), end(container))}); return arena->addType(T{std::vector<TypeId>(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 } // namespace Luau

View file

@ -374,7 +374,7 @@ struct TypeStringifier
tv->ty); tv->ty);
} }
void stringify(const std::string& name, const Property& prop) void emitKey(const std::string& name)
{ {
if (isIdentifier(name)) if (isIdentifier(name))
state.emit(name); state.emit(name);
@ -385,31 +385,46 @@ struct TypeStringifier
state.emit("\"]"); state.emit("\"]");
} }
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. emitKey(name);
if (prop.isShared())
return stringify(*prop.readType());
// Otherwise emit them separately.
if (auto ty = prop.readType())
{
state.emit("read ");
stringify(*ty);
}
if (prop.readType() && prop.writeType())
state.emit(" + ");
if (auto ty = prop.writeType())
{
state.emit("write ");
stringify(*ty);
}
}
else
stringify(prop.type()); stringify(prop.type());
return;
}
if (prop.readTy)
{
state.emit("read ");
emitKey(name);
stringify(*prop.readTy);
comma = true;
}
if (prop.writeTy)
{
if (comma)
{
state.emit(",");
state.newline();
}
state.emit("write ");
emitKey(name);
stringify(*prop.writeTy);
}
}
void stringify(const std::string& name, const Property& prop)
{
if (FFlag::DebugLuauDeferredConstraintResolution)
return _newStringify(name, prop);
emitKey(name);
stringify(prop.type());
} }
void stringify(TypePackId tp); void stringify(TypePackId tp);
@ -1755,7 +1770,7 @@ std::string toString(const Constraint& constraint, ToStringOptions& opts)
} }
else if constexpr (std::is_same_v<T, HasPropConstraint>) else if constexpr (std::is_same_v<T, HasPropConstraint>)
{ {
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<T, SetPropConstraint>) else if constexpr (std::is_same_v<T, SetPropConstraint>)
{ {
@ -1801,6 +1816,8 @@ std::string toString(const Constraint& constraint, ToStringOptions& opts)
{ {
return "reduce " + tos(c.tp); return "reduce " + tos(c.tp);
} }
else if constexpr (std::is_same_v<T, EqualityConstraint>)
return "equality: " + tos(c.resultType) + " ~ " + tos(c.assignmentType);
else else
static_assert(always_false_v<T>, "Non-exhaustive constraint switch"); static_assert(always_false_v<T>, "Non-exhaustive constraint switch");
}; };

View file

@ -26,7 +26,6 @@ LUAU_FASTINTVARIABLE(LuauTypeMaximumStringifierLength, 500)
LUAU_FASTINTVARIABLE(LuauTableTypeMaximumStringifierLength, 0) LUAU_FASTINTVARIABLE(LuauTableTypeMaximumStringifierLength, 0)
LUAU_FASTINT(LuauTypeInferRecursionLimit) LUAU_FASTINT(LuauTypeInferRecursionLimit)
LUAU_FASTFLAG(LuauInstantiateInSubtyping) LUAU_FASTFLAG(LuauInstantiateInSubtyping)
LUAU_FASTFLAG(DebugLuauReadWriteProperties)
namespace Luau namespace Luau
{ {
@ -632,13 +631,10 @@ Property::Property(TypeId readTy, bool deprecated, const std::string& deprecated
, readTy(readTy) , readTy(readTy)
, writeTy(readTy) , writeTy(readTy)
{ {
LUAU_ASSERT(!FFlag::DebugLuauReadWriteProperties);
} }
Property Property::readonly(TypeId ty) Property Property::readonly(TypeId ty)
{ {
LUAU_ASSERT(FFlag::DebugLuauReadWriteProperties);
Property p; Property p;
p.readTy = ty; p.readTy = ty;
return p; return p;
@ -646,8 +642,6 @@ Property Property::readonly(TypeId ty)
Property Property::writeonly(TypeId ty) Property Property::writeonly(TypeId ty)
{ {
LUAU_ASSERT(FFlag::DebugLuauReadWriteProperties);
Property p; Property p;
p.writeTy = ty; p.writeTy = ty;
return p; return p;
@ -660,8 +654,6 @@ Property Property::rw(TypeId ty)
Property Property::rw(TypeId read, TypeId write) Property Property::rw(TypeId read, TypeId write)
{ {
LUAU_ASSERT(FFlag::DebugLuauReadWriteProperties);
Property p; Property p;
p.readTy = read; p.readTy = read;
p.writeTy = write; p.writeTy = write;
@ -683,29 +675,15 @@ Property Property::create(std::optional<TypeId> read, std::optional<TypeId> writ
TypeId Property::type() const TypeId Property::type() const
{ {
LUAU_ASSERT(!FFlag::DebugLuauReadWriteProperties);
LUAU_ASSERT(readTy); LUAU_ASSERT(readTy);
return *readTy; return *readTy;
} }
void Property::setType(TypeId ty) void Property::setType(TypeId ty)
{ {
LUAU_ASSERT(!FFlag::DebugLuauReadWriteProperties);
readTy = ty; readTy = ty;
} if (FFlag::DebugLuauDeferredConstraintResolution)
writeTy = ty;
std::optional<TypeId> Property::readType() const
{
LUAU_ASSERT(FFlag::DebugLuauReadWriteProperties);
LUAU_ASSERT(!(bool(readTy) && bool(writeTy)));
return readTy;
}
std::optional<TypeId> Property::writeType() const
{
LUAU_ASSERT(FFlag::DebugLuauReadWriteProperties);
LUAU_ASSERT(!(bool(readTy) && bool(writeTy)));
return writeTy;
} }
bool Property::isShared() const bool Property::isShared() const
@ -713,6 +691,21 @@ bool Property::isShared() const
return readTy && writeTy && readTy == writeTy; return readTy && writeTy && readTy == writeTy;
} }
bool Property::isReadOnly() const
{
return readTy && !writeTy;
}
bool Property::isWriteOnly() const
{
return !readTy && writeTy;
}
bool Property::isReadWrite() const
{
return readTy && writeTy;
}
TableType::TableType(TableState state, TypeLevel level, Scope* scope) TableType::TableType(TableState state, TypeLevel level, Scope* scope)
: state(state) : state(state)
, level(level) , level(level)
@ -961,6 +954,7 @@ BuiltinTypes::BuiltinTypes()
, optionalStringType(arena->addType(Type{UnionType{{stringType, nilType}}, /*persistent*/ true})) , optionalStringType(arena->addType(Type{UnionType{{stringType, nilType}}, /*persistent*/ true}))
, emptyTypePack(arena->addTypePack(TypePackVar{TypePack{{}}, /*persistent*/ true})) , emptyTypePack(arena->addTypePack(TypePackVar{TypePack{{}}, /*persistent*/ true}))
, anyTypePack(arena->addTypePack(TypePackVar{VariadicTypePack{anyType}, /*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})) , neverTypePack(arena->addTypePack(TypePackVar{VariadicTypePack{neverType}, /*persistent*/ true}))
, uninhabitableTypePack(arena->addTypePack(TypePackVar{TypePack{{neverType}, neverTypePack}, /*persistent*/ true})) , uninhabitableTypePack(arena->addTypePack(TypePackVar{TypePack{{neverType}, neverTypePack}, /*persistent*/ true}))
, errorTypePack(arena->addTypePack(TypePackVar{Unifiable::Error{}, /*persistent*/ true})) , errorTypePack(arena->addTypePack(TypePackVar{Unifiable::Error{}, /*persistent*/ true}))

View file

@ -1460,7 +1460,7 @@ struct TypeChecker2
{ {
visit(expr, ValueContext::RValue); visit(expr, ValueContext::RValue);
TypeId leftType = stripFromNilAndReport(lookupType(expr), location); TypeId leftType = stripFromNilAndReport(lookupType(expr), location);
checkIndexTypeFromType(leftType, propName, location, context, astIndexExprTy); checkIndexTypeFromType(leftType, propName, context, location, astIndexExprTy);
} }
void visit(AstExprIndexName* indexName, ValueContext context) void visit(AstExprIndexName* indexName, ValueContext context)
@ -1709,8 +1709,8 @@ struct TypeChecker2
TypeId visit(AstExprBinary* expr, AstNode* overrideKey = nullptr) TypeId visit(AstExprBinary* expr, AstNode* overrideKey = nullptr)
{ {
visit(expr->left, ValueContext::LValue); visit(expr->left, ValueContext::RValue);
visit(expr->right, ValueContext::LValue); visit(expr->right, ValueContext::RValue);
NotNull<Scope> scope = stack.back(); NotNull<Scope> scope = stack.back();
@ -2534,20 +2534,16 @@ struct TypeChecker2
reportError(std::move(e)); reportError(std::move(e));
} }
// If the provided type does not have the named property, report an error. /* A helper for checkIndexTypeFromType.
void checkIndexTypeFromType(TypeId tableTy, const std::string& prop, const Location& location, ValueContext context, TypeId astIndexExprType) *
* 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<bool, std::vector<TypeId>> lookupProp(const NormalizedType* norm, const std::string& prop, ValueContext context,
const Location& location, TypeId astIndexExprType, std::vector<TypeError>& 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; bool foundOneProp = false;
std::vector<TypeId> typesMissingTheProp; std::vector<TypeId> typesMissingTheProp;
@ -2556,7 +2552,7 @@ struct TypeChecker2
return; return;
DenseHashSet<TypeId> seen{nullptr}; DenseHashSet<TypeId> seen{nullptr};
bool found = hasIndexTypeFromType(ty, prop, location, seen, astIndexExprType); bool found = hasIndexTypeFromType(ty, prop, context, location, seen, astIndexExprType, errors);
foundOneProp |= found; foundOneProp |= found;
if (!found) if (!found)
typesMissingTheProp.push_back(ty); typesMissingTheProp.push_back(ty);
@ -2601,6 +2597,26 @@ struct TypeChecker2
fetch(tyvar); 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<TypeError> dummy;
const auto [foundOneProp, typesMissingTheProp] = lookupProp(norm, prop, context, location, astIndexExprType, module->errors);
if (!typesMissingTheProp.empty()) if (!typesMissingTheProp.empty())
{ {
if (foundOneProp) if (foundOneProp)
@ -2611,17 +2627,29 @@ struct TypeChecker2
// the `else` branch. // the `else` branch.
else if (context == ValueContext::LValue && !get<ClassType>(tableTy)) else if (context == ValueContext::LValue && !get<ClassType>(tableTy))
{ {
if (get<PrimitiveType>(tableTy) || get<FunctionType>(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<PrimitiveType>(tableTy) || get<FunctionType>(tableTy))
reportError(NotATable{tableTy}, location); reportError(NotATable{tableTy}, location);
else else
reportError(CannotExtendTable{tableTy, CannotExtendTable::Property, prop}, location); reportError(CannotExtendTable{tableTy, CannotExtendTable::Property, prop}, location);
} }
else if (context == ValueContext::RValue && !get<ClassType>(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 else
reportError(UnknownProperty{tableTy, prop}, location); reportError(UnknownProperty{tableTy, prop}, location);
} }
} }
bool hasIndexTypeFromType(TypeId ty, const std::string& prop, const Location& location, DenseHashSet<TypeId>& seen, TypeId astIndexExprType) bool hasIndexTypeFromType(TypeId ty, const std::string& prop, ValueContext context, const Location& location, DenseHashSet<TypeId>& seen,
TypeId astIndexExprType, std::vector<TypeError>& errors)
{ {
// If we have already encountered this type, we must assume that some // If we have already encountered this type, we must assume that some
// other codepath will do the right thing and signal false if the // other codepath will do the right thing and signal false if the
@ -2635,14 +2663,14 @@ struct TypeChecker2
if (isString(ty)) if (isString(ty))
{ {
std::optional<TypeId> mtIndex = Luau::findMetatableEntry(builtinTypes, module->errors, builtinTypes->stringType, "__index", location); std::optional<TypeId> mtIndex = Luau::findMetatableEntry(builtinTypes, errors, builtinTypes->stringType, "__index", location);
LUAU_ASSERT(mtIndex); LUAU_ASSERT(mtIndex);
ty = *mtIndex; ty = *mtIndex;
} }
if (auto tt = getTableType(ty)) if (auto tt = getTableType(ty))
{ {
if (findTablePropertyRespectingMeta(builtinTypes, module->errors, ty, prop, location)) if (findTablePropertyRespectingMeta(builtinTypes, errors, ty, prop, context, location))
return true; return true;
if (tt->indexer) if (tt->indexer)
@ -2674,11 +2702,11 @@ struct TypeChecker2
} }
else if (const UnionType* utv = get<UnionType>(ty)) else if (const UnionType* utv = get<UnionType>(ty))
return std::all_of(begin(utv), end(utv), [&](TypeId part) { 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<IntersectionType>(ty)) else if (const IntersectionType* itv = get<IntersectionType>(ty))
return std::any_of(begin(itv), end(itv), [&](TypeId part) { 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 else
return false; return false;

View file

@ -7,6 +7,7 @@
#include "Luau/DenseHash.h" #include "Luau/DenseHash.h"
#include "Luau/Instantiation.h" #include "Luau/Instantiation.h"
#include "Luau/Normalize.h" #include "Luau/Normalize.h"
#include "Luau/NotNull.h"
#include "Luau/Simplify.h" #include "Luau/Simplify.h"
#include "Luau/Substitution.h" #include "Luau/Substitution.h"
#include "Luau/Subtyping.h" #include "Luau/Subtyping.h"
@ -853,6 +854,27 @@ static TypeFamilyReductionResult<TypeId> comparisonFamilyFn(TypeId instance, con
TypeId lhsTy = follow(typeParams.at(0)); TypeId lhsTy = follow(typeParams.at(0));
TypeId rhsTy = follow(typeParams.at(1)); 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<t, t> -> bool
// lt< t, 'a> -> same as above
bool canSubmitConstraint = ctx->solver && ctx->constraint;
if (canSubmitConstraint)
{
if (get<FreeType>(lhsTy) && get<NeverType>(rhsTy) == nullptr)
{
auto c1 = ctx->solver->pushConstraint(ctx->scope, {}, EqualityConstraint{lhsTy, rhsTy});
const_cast<Constraint*>(ctx->constraint)->dependencies.emplace_back(c1);
}
else if (get<FreeType>(rhsTy) && get<NeverType>(lhsTy) == nullptr)
{
auto c1 = ctx->solver->pushConstraint(ctx->scope, {}, EqualityConstraint{rhsTy, lhsTy});
const_cast<Constraint*>(ctx->constraint)->dependencies.emplace_back(c1);
}
}
// check to see if both operand types are resolved enough, and wait to reduce if not // check to see if both operand types are resolved enough, and wait to reduce if not
if (isPending(lhsTy, ctx->solver)) if (isPending(lhsTy, ctx->solver))
return {std::nullopt, false, {lhsTy}, {}}; return {std::nullopt, false, {lhsTy}, {}};
@ -1248,8 +1270,8 @@ TypeFamilyReductionResult<TypeId> keyofFamilyImpl(
if (!normTy) if (!normTy)
return {std::nullopt, false, {}, {}}; 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 // 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
// well) // as well)
if (normTy->hasTables() == normTy->hasClasses()) if (normTy->hasTables() == normTy->hasClasses())
return {std::nullopt, true, {}, {}}; return {std::nullopt, true, {}, {}};

View file

@ -13,7 +13,7 @@
#include <sstream> #include <sstream>
#include <type_traits> #include <type_traits>
LUAU_FASTFLAG(DebugLuauReadWriteProperties); LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution);
// Maximum number of steps to follow when traversing a path. May not always // 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 // equate to the number of components in a path, depending on the traversal
@ -29,7 +29,7 @@ namespace TypePath
Property::Property(std::string name) Property::Property(std::string name)
: name(std::move(name)) : name(std::move(name))
{ {
LUAU_ASSERT(!FFlag::DebugLuauReadWriteProperties); LUAU_ASSERT(!FFlag::DebugLuauDeferredConstraintResolution);
} }
Property Property::read(std::string name) Property Property::read(std::string name)
@ -146,21 +146,21 @@ Path PathBuilder::build()
PathBuilder& PathBuilder::readProp(std::string name) PathBuilder& PathBuilder::readProp(std::string name)
{ {
LUAU_ASSERT(FFlag::DebugLuauReadWriteProperties); LUAU_ASSERT(FFlag::DebugLuauDeferredConstraintResolution);
components.push_back(Property{std::move(name), true}); components.push_back(Property{std::move(name), true});
return *this; return *this;
} }
PathBuilder& PathBuilder::writeProp(std::string name) PathBuilder& PathBuilder::writeProp(std::string name)
{ {
LUAU_ASSERT(FFlag::DebugLuauReadWriteProperties); LUAU_ASSERT(FFlag::DebugLuauDeferredConstraintResolution);
components.push_back(Property{std::move(name), false}); components.push_back(Property{std::move(name), false});
return *this; return *this;
} }
PathBuilder& PathBuilder::prop(std::string name) PathBuilder& PathBuilder::prop(std::string name)
{ {
LUAU_ASSERT(!FFlag::DebugLuauReadWriteProperties); LUAU_ASSERT(!FFlag::DebugLuauDeferredConstraintResolution);
components.push_back(Property{std::move(name)}); components.push_back(Property{std::move(name)});
return *this; return *this;
} }
@ -323,7 +323,7 @@ struct TraversalState
// logic there. // logic there.
updateCurrent(*m); updateCurrent(*m);
if (!traverse(TypePath::Property{"__index"})) if (!traverse(TypePath::Property::read("__index")))
return false; return false;
return traverse(property); return traverse(property);
@ -333,8 +333,8 @@ struct TraversalState
if (prop) if (prop)
{ {
std::optional<TypeId> maybeType; std::optional<TypeId> maybeType;
if (FFlag::DebugLuauReadWriteProperties) if (FFlag::DebugLuauDeferredConstraintResolution)
maybeType = property.isRead ? prop->readType() : prop->writeType(); maybeType = property.isRead ? prop->readTy : prop->writeTy;
else else
maybeType = prop->type(); maybeType = prop->type();
@ -514,7 +514,7 @@ std::string toString(const TypePath::Path& path, bool prefixDot)
if constexpr (std::is_same_v<T, TypePath::Property>) if constexpr (std::is_same_v<T, TypePath::Property>)
{ {
result << '['; result << '[';
if (FFlag::DebugLuauReadWriteProperties) if (FFlag::DebugLuauDeferredConstraintResolution)
{ {
if (c.isRead) if (c.isRead)
result << "read "; result << "read ";

View file

@ -44,6 +44,12 @@ std::optional<TypeId> findMetatableEntry(
std::optional<TypeId> findTablePropertyRespectingMeta( std::optional<TypeId> findTablePropertyRespectingMeta(
NotNull<BuiltinTypes> builtinTypes, ErrorVec& errors, TypeId ty, const std::string& name, Location location) NotNull<BuiltinTypes> builtinTypes, ErrorVec& errors, TypeId ty, const std::string& name, Location location)
{
return findTablePropertyRespectingMeta(builtinTypes, errors, ty, name, ValueContext::RValue, location);
}
std::optional<TypeId> findTablePropertyRespectingMeta(
NotNull<BuiltinTypes> builtinTypes, ErrorVec& errors, TypeId ty, const std::string& name, ValueContext context, Location location)
{ {
if (get<AnyType>(ty)) if (get<AnyType>(ty))
return ty; return ty;
@ -52,7 +58,20 @@ std::optional<TypeId> findTablePropertyRespectingMeta(
{ {
const auto& it = tableType->props.find(name); const auto& it = tableType->props.find(name);
if (it != tableType->props.end()) 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<TypeId> mtIndex = findMetatableEntry(builtinTypes, errors, ty, "__index", location); std::optional<TypeId> mtIndex = findMetatableEntry(builtinTypes, errors, ty, "__index", location);

View file

@ -8,6 +8,7 @@
#include "Luau/Type.h" #include "Luau/Type.h"
#include "Luau/TypeArena.h" #include "Luau/TypeArena.h"
#include "Luau/TypeCheckLimits.h" #include "Luau/TypeCheckLimits.h"
#include "Luau/TypeFamily.h"
#include "Luau/TypeUtils.h" #include "Luau/TypeUtils.h"
#include "Luau/VisitType.h" #include "Luau/VisitType.h"
@ -19,6 +20,59 @@ LUAU_FASTINT(LuauTypeInferRecursionLimit)
namespace Luau namespace Luau
{ {
static bool areCompatible(TypeId left, TypeId right)
{
auto p = get2<TableType, TableType>(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<FreeType>(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<TypeArena> arena, NotNull<BuiltinTypes> builtinTypes, NotNull<Scope> scope, NotNull<InternalErrorReporter> ice) Unifier2::Unifier2(NotNull<TypeArena> arena, NotNull<BuiltinTypes> builtinTypes, NotNull<Scope> scope, NotNull<InternalErrorReporter> ice)
: arena(arena) : arena(arena)
, builtinTypes(builtinTypes) , builtinTypes(builtinTypes)
@ -34,6 +88,12 @@ bool Unifier2::unify(TypeId subTy, TypeId superTy)
subTy = follow(subTy); subTy = follow(subTy);
superTy = follow(superTy); 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})) if (seenTypePairings.contains({subTy, superTy}))
return true; return true;
seenTypePairings.insert({subTy, superTy}); seenTypePairings.insert({subTy, superTy});
@ -44,14 +104,21 @@ bool Unifier2::unify(TypeId subTy, TypeId superTy)
FreeType* subFree = getMutable<FreeType>(subTy); FreeType* subFree = getMutable<FreeType>(subTy);
FreeType* superFree = getMutable<FreeType>(superTy); FreeType* superFree = getMutable<FreeType>(superTy);
if (subFree) if (subFree && superFree)
{
superFree->lowerBound = mkUnion(subFree->lowerBound, superFree->lowerBound);
superFree->upperBound = mkIntersection(subFree->upperBound, superFree->upperBound);
asMutable(subTy)->ty.emplace<BoundType>(superTy);
}
else if (subFree)
{ {
subFree->upperBound = mkIntersection(subFree->upperBound, superTy); subFree->upperBound = mkIntersection(subFree->upperBound, superTy);
expandedFreeTypes[subTy].push_back(superTy); expandedFreeTypes[subTy].push_back(superTy);
} }
else if (superFree)
if (superFree) {
superFree->lowerBound = mkUnion(superFree->lowerBound, subTy); superFree->lowerBound = mkUnion(superFree->lowerBound, subTy);
}
if (subFree || superFree) if (subFree || superFree)
return true; return true;
@ -159,13 +226,11 @@ bool Unifier2::unify(TypeId subTy, const FunctionType* superFn)
if (shouldInstantiate) if (shouldInstantiate)
{ {
std::optional<TypeId> instantiated = instantiate(builtinTypes, arena, NotNull{&limits}, scope, subTy); for (auto generic : subFn->generics)
if (!instantiated) genericSubstitutions[generic] = freshType(arena, builtinTypes, scope);
return false;
subFn = get<FunctionType>(*instantiated); for (auto genericPack : subFn->genericPacks)
genericPackSubstitutions[genericPack] = arena->freshTypePack(scope);
LUAU_ASSERT(subFn); // instantiation should not make a function type _not_ a function type.
} }
bool argResult = unify(superFn->argTypes, subFn->argTypes); 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 // if the occurs check fails for any option, it fails overall
for (auto subOption : subUnion->options) for (auto subOption : subUnion->options)
result &= unify(subOption, superTy); {
if (areCompatible(subOption, superTy))
result &= unify(subOption, superTy);
}
return result; 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 // if the occurs check fails for any option, it fails overall
for (auto superOption : superUnion->options) for (auto superOption : superUnion->options)
result &= unify(subTy, superOption); {
if (areCompatible(subTy, superOption))
result &= unify(subTy, superOption);
}
return result; return result;
} }
@ -228,7 +299,21 @@ bool Unifier2::unify(TableType* subTable, const TableType* superTable)
auto superPropOpt = superTable->props.find(propName); auto superPropOpt = superTable->props.find(propName);
if (superPropOpt != superTable->props.end()) 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(); auto subTypeParamsIter = subTable->instantiatedTypeParams.begin();
@ -293,10 +378,19 @@ bool Unifier2::unify(TypePackId subTp, TypePackId superTp)
subTp = follow(subTp); subTp = follow(subTp);
superTp = follow(superTp); 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})) if (seenTypePackPairings.contains({subTp, superTp}))
return true; return true;
seenTypePackPairings.insert({subTp, superTp}); seenTypePackPairings.insert({subTp, superTp});
if (subTp == superTp)
return true;
const FreeTypePack* subFree = get<FreeTypePack>(subTp); const FreeTypePack* subFree = get<FreeTypePack>(subTp);
const FreeTypePack* superFree = get<FreeTypePack>(superTp); const FreeTypePack* superFree = get<FreeTypePack>(superTp);
@ -378,11 +472,14 @@ struct FreeTypeSearcher : TypeVisitor
{ {
} }
enum enum Polarity
{ {
Positive, Positive,
Negative Negative,
} polarity = Positive; Both,
};
Polarity polarity = Positive;
void flip() void flip()
{ {
@ -394,6 +491,8 @@ struct FreeTypeSearcher : TypeVisitor
case Negative: case Negative:
polarity = Positive; polarity = Positive;
break; break;
case Both:
break;
} }
} }
@ -419,6 +518,10 @@ struct FreeTypeSearcher : TypeVisitor
case Negative: case Negative:
negativeTypes[ty]++; negativeTypes[ty]++;
break; break;
case Both:
positiveTypes[ty]++;
negativeTypes[ty]++;
break;
} }
return true; return true;
@ -436,10 +539,35 @@ struct FreeTypeSearcher : TypeVisitor
case Negative: case Negative:
negativeTypes[ty]++; negativeTypes[ty]++;
break; 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 bool visit(TypeId ty, const FunctionType& ft) override
@ -538,8 +666,8 @@ struct MutatingGeneralizer : TypeOnceVisitor
if (!ft) if (!ft)
return false; return false;
const bool positiveCount = getCount(positiveTypes, ty); const size_t positiveCount = getCount(positiveTypes, ty);
const bool negativeCount = getCount(negativeTypes, ty); const size_t negativeCount = getCount(negativeTypes, ty);
if (!positiveCount && !negativeCount) if (!positiveCount && !negativeCount)
return false; return false;

View file

@ -18,6 +18,7 @@ LUAU_FASTINTVARIABLE(LuauParseErrorLimit, 100)
// See docs/SyntaxChanges.md for an explanation. // See docs/SyntaxChanges.md for an explanation.
LUAU_FASTFLAG(LuauCheckedFunctionSyntax) LUAU_FASTFLAG(LuauCheckedFunctionSyntax)
LUAU_FASTFLAGVARIABLE(LuauReadWritePropertySyntax, false) LUAU_FASTFLAGVARIABLE(LuauReadWritePropertySyntax, false)
LUAU_FASTFLAGVARIABLE(DebugLuauDeferredConstraintResolution, false)
namespace Luau namespace Luau
{ {
@ -1339,7 +1340,7 @@ AstType* Parser::parseTableType(bool inDeclarationContext)
AstTableAccess access = AstTableAccess::ReadWrite; AstTableAccess access = AstTableAccess::ReadWrite;
std::optional<Location> accessLocation; std::optional<Location> accessLocation;
if (FFlag::LuauReadWritePropertySyntax) if (FFlag::LuauReadWritePropertySyntax || FFlag::DebugLuauDeferredConstraintResolution)
{ {
if (lexer.current().type == Lexeme::Name && lexer.lookahead().type != ':') if (lexer.current().type == Lexeme::Name && lexer.lookahead().type != ':')
{ {

View file

@ -431,6 +431,10 @@ std::vector<std::string> getSourceFiles(int argc, char** argv)
for (int i = 1; i < argc; ++i) 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 // Treat '-' as a special file whose source is read from stdin
// All other arguments that start with '-' are skipped // All other arguments that start with '-' are skipped
if (argv[i][0] == '-' && argv[i][1] != '\0') if (argv[i][0] == '-' && argv[i][1] != '\0')

View file

@ -152,7 +152,7 @@ struct Reducer
} }
#if VERBOSE >= 1 #if VERBOSE >= 1
printf("running %s\n", command.c_str()); printf("running %s\n", cmd.c_str());
#endif #endif
TestResult result = TestResult::NoBug; TestResult result = TestResult::NoBug;
@ -160,7 +160,7 @@ struct Reducer
++step; ++step;
printf("Step %4d...\n", step); printf("Step %4d...\n", step);
FILE* p = popen(command.c_str(), "r"); FILE* p = popen(cmd.c_str(), "r");
while (!feof(p)) while (!feof(p))
{ {

View file

@ -46,6 +46,8 @@ LUAU_FASTFLAGVARIABLE(LuauUpdatedRequireByStringSemantics, false)
constexpr int MaxTraversalLimit = 50; constexpr int MaxTraversalLimit = 50;
static bool codegen = false; static bool codegen = false;
static int program_argc = 0;
char** program_argv = nullptr;
// Ctrl-C handling // Ctrl-C handling
static void sigintCallback(lua_State* L, int gc) static void sigintCallback(lua_State* L, int gc)
@ -318,6 +320,12 @@ void setupState(lua_State* L)
luaL_sandbox(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 runCode(lua_State* L, const std::string& source)
{ {
std::string bytecode = Luau::compile(source, copts()); std::string bytecode = Luau::compile(source, copts());
@ -668,7 +676,8 @@ static bool runFile(const char* name, lua_State* GL, bool repl)
if (coverageActive()) if (coverageActive())
coverageTrack(L, -1); coverageTrack(L, -1);
status = lua_resume(L, NULL, 0); setupArguments(L, program_argc, program_argv);
status = lua_resume(L, NULL, program_argc);
} }
else else
{ {
@ -704,7 +713,7 @@ static bool runFile(const char* name, lua_State* GL, bool repl)
static void displayHelp(const char* argv0) 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("\n");
printf("When file list is omitted, an interactive REPL is started instead.\n"); printf("When file list is omitted, an interactive REPL is started instead.\n");
printf("\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(" --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(" --timetrace: record compiler time tracing information into trace.json\n");
printf(" --codegen: execute code using native code generation\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) 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 coverage = false;
bool interactive = false; bool interactive = false;
bool codegenPerf = false; bool codegenPerf = false;
int program_args = argc;
for (int i = 1; i < argc; i++) for (int i = 1; i < argc; i++)
{ {
@ -800,6 +811,11 @@ int replMain(int argc, char** argv)
{ {
setLuauFlags(argv[i] + 9); 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] == '-') else if (argv[i][0] == '-')
{ {
fprintf(stderr, "Error: Unrecognized option '%s'.\n\n", argv[i]); 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 !defined(LUAU_ENABLE_TIME_TRACE)
if (FFlag::DebugLuauTimeTracing) if (FFlag::DebugLuauTimeTracing)
{ {

View file

@ -57,8 +57,6 @@ LUAU_FASTINTVARIABLE(CodegenHeuristicsBlockLimit, 32'768) // 32 K
// Current value is based on some member variables being limited to 16 bits // Current value is based on some member variables being limited to 16 bits
LUAU_FASTINTVARIABLE(CodegenHeuristicsBlockInstructionLimit, 65'536) // 64 K LUAU_FASTINTVARIABLE(CodegenHeuristicsBlockInstructionLimit, 65'536) // 64 K
LUAU_FASTFLAGVARIABLE(DisableNativeCodegenIfBreakpointIsSet, false)
namespace Luau namespace Luau
{ {
namespace CodeGen namespace CodeGen
@ -302,7 +300,7 @@ void create(lua_State* L, AllocationCallback* allocationCallback, void* allocati
ecb->close = onCloseState; ecb->close = onCloseState;
ecb->destroy = onDestroyFunction; ecb->destroy = onDestroyFunction;
ecb->enter = onEnter; ecb->enter = onEnter;
ecb->disable = FFlag::DisableNativeCodegenIfBreakpointIsSet ? onDisable : nullptr; ecb->disable = onDisable;
} }
void create(lua_State* L) void create(lua_State* L)

View file

@ -3,6 +3,8 @@
#include "Luau/IrUtils.h" #include "Luau/IrUtils.h"
LUAU_DYNAMIC_FASTFLAGVARIABLE(LuauCodegenTrackingMultilocationFix, false)
namespace Luau namespace Luau
{ {
namespace CodeGen namespace CodeGen
@ -159,6 +161,9 @@ void IrValueLocationTracking::afterInstLowering(IrInst& inst, uint32_t instIdx)
case IrCmd::LOAD_DOUBLE: case IrCmd::LOAD_DOUBLE:
case IrCmd::LOAD_INT: case IrCmd::LOAD_INT:
case IrCmd::LOAD_TVALUE: case IrCmd::LOAD_TVALUE:
if (DFFlag::LuauCodegenTrackingMultilocationFix && inst.a.kind == IrOpKind::VmReg)
invalidateRestoreOp(inst.a, /*skipValueInvalidation*/ false);
recordRestoreOp(instIdx, inst.a); recordRestoreOp(instIdx, inst.a);
break; break;
case IrCmd::STORE_POINTER: case IrCmd::STORE_POINTER:

View file

@ -176,6 +176,7 @@ target_sources(Luau.Analysis PRIVATE
Analysis/include/Luau/GlobalTypes.h Analysis/include/Luau/GlobalTypes.h
Analysis/include/Luau/InsertionOrderedMap.h Analysis/include/Luau/InsertionOrderedMap.h
Analysis/include/Luau/Instantiation.h Analysis/include/Luau/Instantiation.h
Analysis/include/Luau/Instantiation2.h
Analysis/include/Luau/IostreamHelpers.h Analysis/include/Luau/IostreamHelpers.h
Analysis/include/Luau/JsonEmitter.h Analysis/include/Luau/JsonEmitter.h
Analysis/include/Luau/Linter.h Analysis/include/Luau/Linter.h
@ -242,6 +243,7 @@ target_sources(Luau.Analysis PRIVATE
Analysis/src/Frontend.cpp Analysis/src/Frontend.cpp
Analysis/src/GlobalTypes.cpp Analysis/src/GlobalTypes.cpp
Analysis/src/Instantiation.cpp Analysis/src/Instantiation.cpp
Analysis/src/Instantiation2.cpp
Analysis/src/IostreamHelpers.cpp Analysis/src/IostreamHelpers.cpp
Analysis/src/JsonEmitter.cpp Analysis/src/JsonEmitter.cpp
Analysis/src/Linter.cpp Analysis/src/Linter.cpp
@ -457,7 +459,6 @@ if(TARGET Luau.UnitTest)
tests/TypeInfer.primitives.test.cpp tests/TypeInfer.primitives.test.cpp
tests/TypeInfer.provisional.test.cpp tests/TypeInfer.provisional.test.cpp
tests/TypeInfer.refinements.test.cpp tests/TypeInfer.refinements.test.cpp
tests/TypeInfer.rwprops.test.cpp
tests/TypeInfer.singletons.test.cpp tests/TypeInfer.singletons.test.cpp
tests/TypeInfer.tables.test.cpp tests/TypeInfer.tables.test.cpp
tests/TypeInfer.test.cpp tests/TypeInfer.test.cpp

View file

@ -31,6 +31,7 @@ LUAU_FASTFLAG(LuauSciNumberSkipTrailDot)
LUAU_DYNAMIC_FASTFLAG(LuauInterruptablePatternMatch) LUAU_DYNAMIC_FASTFLAG(LuauInterruptablePatternMatch)
LUAU_FASTINT(CodegenHeuristicsInstructionLimit) LUAU_FASTINT(CodegenHeuristicsInstructionLimit)
LUAU_DYNAMIC_FASTFLAG(LuauCodeGenFixBufferLenCheckA64) LUAU_DYNAMIC_FASTFLAG(LuauCodeGenFixBufferLenCheckA64)
LUAU_DYNAMIC_FASTFLAG(LuauCodegenTrackingMultilocationFix)
static lua_CompileOptions defaultOptions() static lua_CompileOptions defaultOptions()
{ {
@ -2040,6 +2041,7 @@ TEST_CASE("SafeEnv")
TEST_CASE("Native") TEST_CASE("Native")
{ {
ScopedFastFlag luauCodeGenFixBufferLenCheckA64{DFFlag::LuauCodeGenFixBufferLenCheckA64, true}; ScopedFastFlag luauCodeGenFixBufferLenCheckA64{DFFlag::LuauCodeGenFixBufferLenCheckA64, true};
ScopedFastFlag luauCodegenTrackingMultilocationFix{DFFlag::LuauCodegenTrackingMultilocationFix, true};
// This tests requires code to run natively, otherwise all 'is_native' checks will fail // This tests requires code to run natively, otherwise all 'is_native' checks will fail
if (!codegen || !luau_codegen_supported()) if (!codegen || !luau_codegen_supported())

View file

@ -17,7 +17,7 @@ static TypeId requireBinding(Scope* scope, const char* name)
TEST_SUITE_BEGIN("ConstraintSolver"); TEST_SUITE_BEGIN("ConstraintSolver");
TEST_CASE_FIXTURE(ConstraintGeneratorFixture, "hello") TEST_CASE_FIXTURE(ConstraintGeneratorFixture, "constraint_basics")
{ {
solve(R"( solve(R"(
local a = 55 local a = 55
@ -58,12 +58,7 @@ TEST_CASE_FIXTURE(ConstraintGeneratorFixture, "proper_let_generalization")
TypeId idType = requireBinding(rootScope, "b"); TypeId idType = requireBinding(rootScope, "b");
ToStringOptions opts; CHECK("(unknown) -> number" == toString(idType));
if (FFlag::DebugLuauDeferredConstraintResolution)
CHECK("(unknown) -> number" == toString(idType, opts));
else
CHECK("<a>(a) -> number" == toString(idType, opts));
} }
TEST_SUITE_END(); TEST_SUITE_END();

View file

@ -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") TEST_CASE_FIXTURE(DifferFixture, "equal_table_two_cyclic_tables_are_not_different")
{ {
ScopedFastFlag sff{FFlag::DebugLuauDeferredConstraintResolution, false};
CheckResult result = check(R"( CheckResult result = check(R"(
local function id<a>(x: a): a local function id<a>(x: a): a
return x return x

View file

@ -7,6 +7,8 @@
#include "doctest.h" #include "doctest.h"
LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution);
using namespace Luau; using namespace Luau;
TEST_SUITE_BEGIN("Linter"); 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"); 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") TEST_CASE_FIXTURE(Fixture, "ImportOnlyUsedInTypeAnnotation")
{ {
LintResult result = lint(R"( LintResult result = lint(R"(

View file

@ -940,4 +940,26 @@ TEST_CASE_FIXTURE(NormalizeFixture, "normalize_unknown")
CHECK(toString(normalizer.typeFromNormal(*nt)) == "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(); TEST_SUITE_END();

View file

@ -15,6 +15,8 @@
#include <initializer_list> #include <initializer_list>
LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution);
using namespace Luau; using namespace Luau;
namespace Luau namespace Luau
@ -65,6 +67,8 @@ struct SubtypeFixture : Fixture
UnifierSharedState sharedState{&ice}; UnifierSharedState sharedState{&ice};
Normalizer normalizer{&arena, builtinTypes, NotNull{&sharedState}}; Normalizer normalizer{&arena, builtinTypes, NotNull{&sharedState}};
ScopedFastFlag sff{FFlag::DebugLuauDeferredConstraintResolution, true};
ScopePtr rootScope{new Scope(builtinTypes->emptyTypePack)}; ScopePtr rootScope{new Scope(builtinTypes->emptyTypePack)};
ScopePtr moduleScope{new Scope(rootScope)}; ScopePtr moduleScope{new Scope(rootScope)};
@ -220,6 +224,11 @@ struct SubtypeFixture : Fixture
{"Y", builtinTypes->numberType}, {"Y", builtinTypes->numberType},
}); });
TypeId readOnlyVec2Class = cls("ReadOnlyVec2", {
{"X", Property::readonly(builtinTypes->numberType)},
{"Y", Property::readonly(builtinTypes->numberType)},
});
// "hello" | "hello" // "hello" | "hello"
TypeId helloOrHelloType = arena.addType(UnionType{{helloType, helloType}}); TypeId helloOrHelloType = arena.addType(UnionType{{helloType, helloType}});
@ -787,6 +796,34 @@ TEST_CASE_FIXTURE(SubtypeFixture, "{x: <T>(T) -> ()} <: {x: <U>(U) -> ()}")
CHECK_IS_SUBTYPE(tbl({{"x", genericTToNothingType}}), tbl({{"x", genericUToNothingType}})); 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 {} }") TEST_CASE_FIXTURE(SubtypeFixture, "{ @metatable { x: number } } <: { @metatable {} }")
{ {
CHECK_IS_SUBTYPE(meta({{"x", builtinTypes->numberType}}), meta({})); CHECK_IS_SUBTYPE(meta({{"x", builtinTypes->numberType}}), meta({}));
@ -1027,6 +1064,28 @@ TEST_CASE_FIXTURE(SubtypeFixture, "Vec2 <!: table & { X: number, Y: number }")
CHECK_IS_NOT_SUBTYPE(vec2Class, meet(builtinTypes->tableType, xy)); CHECK_IS_NOT_SUBTYPE(vec2Class, meet(builtinTypes->tableType, xy));
} }
TEST_CASE_FIXTURE(SubtypeFixture, "ReadOnlyVec2 <!: { X: number, Y: number}")
{
CHECK_IS_NOT_SUBTYPE(readOnlyVec2Class, tbl({{"X", builtinTypes->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 }") TEST_CASE_FIXTURE(SubtypeFixture, "\"hello\" <: { lower : (string) -> string }")
{ {
CHECK_IS_SUBTYPE(helloType, tableWithLower); CHECK_IS_SUBTYPE(helloType, tableWithLower);
@ -1217,8 +1276,8 @@ TEST_CASE_FIXTURE(SubtypeFixture, "table_property")
SubtypingResult result = isSubtype(subTy, superTy); SubtypingResult result = isSubtype(subTy, superTy);
CHECK(!result.isSubtype); CHECK(!result.isSubtype);
CHECK(result.reasoning == std::vector{SubtypingReasoning{/* subPath */ Path(TypePath::Property("X")), CHECK(result.reasoning == std::vector{SubtypingReasoning{/* subPath */ Path(TypePath::Property::read("X")),
/* superPath */ Path(TypePath::Property("X")), /* superPath */ Path(TypePath::Property::read("X")),
/* variance */ SubtypingVariance::Invariant}}); /* variance */ SubtypingVariance::Invariant}});
} }
@ -1317,8 +1376,8 @@ TEST_CASE_FIXTURE(SubtypeFixture, "nested_table_properties")
SubtypingResult result = isSubtype(subTy, superTy); SubtypingResult result = isSubtype(subTy, superTy);
CHECK(!result.isSubtype); CHECK(!result.isSubtype);
CHECK(result.reasoning == std::vector{SubtypingReasoning{ CHECK(result.reasoning == std::vector{SubtypingReasoning{
/* subPath */ TypePath::PathBuilder().prop("X").prop("Y").prop("Z").build(), /* subPath */ TypePath::PathBuilder().readProp("X").readProp("Y").readProp("Z").build(),
/* superPath */ TypePath::PathBuilder().prop("X").prop("Y").prop("Z").build(), /* superPath */ TypePath::PathBuilder().readProp("X").readProp("Y").readProp("Z").build(),
/* variance */ SubtypingVariance::Invariant, /* 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 // metatable is empty, and abort there, without looking at the metatable
// properties (because there aren't any). // properties (because there aren't any).
CHECK(result.reasoning == std::vector{SubtypingReasoning{ CHECK(result.reasoning == std::vector{SubtypingReasoning{
/* subPath */ TypePath::PathBuilder().mt().prop("__index").build(), /* subPath */ TypePath::PathBuilder().mt().readProp("__index").build(),
/* superPath */ TypePath::kEmpty, /* superPath */ TypePath::kEmpty,
}}); }});
} }
@ -1360,12 +1419,13 @@ TEST_CASE_FIXTURE(SubtypeFixture, "multiple_reasonings")
SubtypingResult result = isSubtype(subTy, superTy); SubtypingResult result = isSubtype(subTy, superTy);
CHECK(!result.isSubtype); CHECK(!result.isSubtype);
CHECK(result.reasoning == std::vector{ CHECK(result.reasoning ==
SubtypingReasoning{/* subPath */ Path(TypePath::Property("X")), /* superPath */ Path(TypePath::Property("X")), std::vector{
/* variance */ SubtypingVariance::Invariant}, SubtypingReasoning{/* subPath */ Path(TypePath::Property::read("X")), /* superPath */ Path(TypePath::Property::read("X")),
SubtypingReasoning{/* subPath */ Path(TypePath::Property("Y")), /* superPath */ Path(TypePath::Property("Y")), /* variance */ SubtypingVariance::Invariant},
/* variance */ SubtypingVariance::Invariant}, SubtypingReasoning{/* subPath */ Path(TypePath::Property::read("Y")), /* superPath */ Path(TypePath::Property::read("Y")),
}); /* variance */ SubtypingVariance::Invariant},
});
} }
TEST_SUITE_END(); TEST_SUITE_END();

View file

@ -931,18 +931,17 @@ TEST_CASE_FIXTURE(Fixture, "tostring_unsee_ttv_if_array")
TEST_CASE_FIXTURE(Fixture, "tostring_error_mismatch") TEST_CASE_FIXTURE(Fixture, "tostring_error_mismatch")
{ {
CheckResult result = check(R"( CheckResult result = check(R"(
--!strict --!strict
function f1() : {a : number, b : string, c : { d : number}} function f1() : {a : number, b : string, c : { d : number}}
return { a = 1, b = "a", c = {d = "a"}} return { a = 1, b = "a", c = {d = "a"}}
end end
)");
)"); std::string expected;
//clang-format off if (FFlag::DebugLuauDeferredConstraintResolution)
std::string expected = 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)";
(FFlag::DebugLuauDeferredConstraintResolution) else
? 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)" expected = R"(Type
:
R"(Type
'{ a: number, b: string, c: { d: string } }' '{ a: number, b: string, c: { d: string } }'
could not be converted into could not be converted into
'{| a: number, b: string, c: {| d: number |} |}' '{| a: number, b: string, c: {| d: number |} |}'
@ -955,7 +954,6 @@ could not be converted into
caused by: caused by:
Property 'd' is not compatible. Property 'd' is not compatible.
Type 'string' could not be converted into 'number' in an invariant context)"; Type 'string' could not be converted into 'number' in an invariant context)";
//clang-format on
LUAU_REQUIRE_ERROR_COUNT(1, result); LUAU_REQUIRE_ERROR_COUNT(1, result);
@ -984,4 +982,20 @@ local f = abs
TypeId fn = requireType("f"); TypeId fn = requireType("f");
CHECK("@checked (number) -> number" == toString(fn)); 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(); TEST_SUITE_END();

View file

@ -202,7 +202,7 @@ TEST_CASE_FIXTURE(Fixture, "generic_aliases")
)"); )");
LUAU_REQUIRE_ERROR_COUNT(1, result); LUAU_REQUIRE_ERROR_COUNT(1, result);
const std::string expected = R"(Type 'bad' could not be converted into 'T<number>'; at ["v"], string is not exactly number)"; const std::string expected = R"(Type 'bad' could not be converted into 'T<number>'; at [read "v"], string is not exactly number)";
CHECK(result.errors[0].location == Location{{4, 31}, {4, 44}}); CHECK(result.errors[0].location == Location{{4, 31}, {4, 44}});
CHECK_EQ(expected, toString(result.errors[0])); CHECK_EQ(expected, toString(result.errors[0]));
} }
@ -221,7 +221,7 @@ TEST_CASE_FIXTURE(Fixture, "dependent_generic_aliases")
)"); )");
LUAU_REQUIRE_ERROR_COUNT(1, result); LUAU_REQUIRE_ERROR_COUNT(1, result);
const std::string expected = R"(Type 'bad' could not be converted into 'U<number>'; at ["t"]["v"], string is not exactly number)"; const std::string expected = R"(Type 'bad' could not be converted into 'U<number>'; at [read "t"][read "v"], string is not exactly number)";
CHECK(result.errors[0].location == Location{{4, 31}, {4, 52}}); CHECK(result.errors[0].location == Location{{4, 31}, {4, 52}});
CHECK_EQ(expected, toString(result.errors[0])); CHECK_EQ(expected, toString(result.errors[0]));

View file

@ -386,10 +386,7 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "setmetatable_on_union_of_tables")
LUAU_REQUIRE_NO_ERRORS(result); LUAU_REQUIRE_NO_ERRORS(result);
if (FFlag::DebugLuauDeferredConstraintResolution) CHECK("{ @metatable { }, A } | { @metatable { }, B }" == toString(requireTypeAlias("X")));
CHECK("{ @metatable {| |}, A } | { @metatable {| |}, B }" == toString(requireTypeAlias("X")));
else
CHECK("{ @metatable { }, A } | { @metatable { }, B }" == toString(requireTypeAlias("X")));
} }
TEST_CASE_FIXTURE(BuiltinsFixture, "table_insert_correctly_infers_type_of_array_2_args_overload") 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"))); 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") TEST_CASE_FIXTURE(BuiltinsFixture, "assert_removes_falsy_types_even_from_type_pack_tail_but_only_for_the_first_type")
{ {
CheckResult result = check(R"( CheckResult result = check(R"(

View file

@ -463,7 +463,7 @@ local b: B = a
LUAU_REQUIRE_ERRORS(result); LUAU_REQUIRE_ERRORS(result);
if (FFlag::DebugLuauDeferredConstraintResolution) 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 else
{ {
const std::string expected = R"(Type 'A' could not be converted into 'B' 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<ClassType>(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<ClassType>(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<TypeMismatch>(result.errors[0]);
REQUIRE(tm);
CHECK(builtinTypes->stringType == tm->wantedType);
CHECK(builtinTypes->numberType == tm->givenType);
}
TEST_SUITE_END(); TEST_SUITE_END();

View file

@ -27,6 +27,8 @@ TEST_CASE_FIXTURE(Fixture, "check_generic_function")
local y: number = id(37) local y: number = id(37)
)"); )");
LUAU_REQUIRE_NO_ERRORS(result); 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") 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) local y: number = id(37)
)"); )");
LUAU_REQUIRE_NO_ERRORS(result); 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<a>(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}) -> 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") 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); LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("{ f: (t1) -> (), id: <a>(unknown, a) -> a } where t1 = { id: ((t1, number) -> number) & ((t1, string) -> string) }", CHECK_EQ("{ f: (t1) -> (), id: <a>(unknown, a) -> a } where t1 = { read id: ((t1, number) -> number) & ((t1, string) -> string) }",
toString(requireType("x"), {true})); toString(requireType("x"), {true}));
} }
else else
@ -437,6 +473,7 @@ TEST_CASE_FIXTURE(Fixture, "dont_leak_generic_types")
-- so this assignment should fail -- so this assignment should fail
local b: boolean = f(true) local b: boolean = f(true)
)"); )");
LUAU_REQUIRE_ERRORS(result); LUAU_REQUIRE_ERRORS(result);
} }
@ -797,8 +834,9 @@ y.a.c = y
LUAU_REQUIRE_ERRORS(result); LUAU_REQUIRE_ERRORS(result);
if (FFlag::DebugLuauDeferredConstraintResolution) if (FFlag::DebugLuauDeferredConstraintResolution)
CHECK(toString(result.errors.at(0)) == CHECK(
R"(Type 'x' could not be converted into 'T<number>'; type x["a"]["c"] (nil) is not exactly T<number>["a"]["c"][0] (T<number>))"); toString(result.errors.at(0)) ==
R"(Type 'x' could not be converted into 'T<number>'; type x[read "a"][read "c"] (nil) is not exactly T<number>[read "a"][read "c"][0] (T<number>))");
else else
{ {
const std::string expected = R"(Type 'y' could not be converted into 'T<string>' const std::string expected = R"(Type 'y' could not be converted into 'T<string>'
@ -1369,6 +1407,19 @@ TEST_CASE_FIXTURE(Fixture, "bidirectional_checking_and_generalization_play_nice"
CHECK("string" == toString(requireType("b"))); 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") TEST_CASE_FIXTURE(Fixture, "missing_generic_type_parameter")
{ {
CheckResult result = check(R"( CheckResult result = check(R"(
@ -1381,4 +1432,20 @@ TEST_CASE_FIXTURE(Fixture, "missing_generic_type_parameter")
REQUIRE(get<UnknownSymbol>(result.errors[1])); REQUIRE(get<UnknownSymbol>(result.errors[1]));
} }
TEST_CASE_FIXTURE(BuiltinsFixture, "generic_type_families_work_in_subtyping")
{
if (!FFlag::DebugLuauDeferredConstraintResolution)
return;
CheckResult result = check(R"(
local function addOne<T>(x: T): add<T, number> return x + 1 end
local function six(): number
return addOne(5)
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_SUITE_END(); TEST_SUITE_END();

View file

@ -414,7 +414,7 @@ local b: B.T = a
LUAU_REQUIRE_ERROR_COUNT(1, result); LUAU_REQUIRE_ERROR_COUNT(1, result);
if (FFlag::DebugLuauDeferredConstraintResolution) 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 else
{ {
const std::string expected = R"(Type 'T' from 'game/A' could not be converted into 'T' from 'game/B' 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); LUAU_REQUIRE_ERROR_COUNT(1, result);
if (FFlag::DebugLuauDeferredConstraintResolution) 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 else
{ {
const std::string expected = R"(Type 'T' from 'game/B' could not be converted into 'T' from 'game/C' const std::string expected = R"(Type 'T' from 'game/B' could not be converted into 'T' from 'game/C'

View file

@ -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<Scope> globalScope{frontend.globals.globalScope.get()};
unfreeze(*arena);
TypeId genericT = arena->addType(GenericType{"T"});
TypeId readonlyX = arena->addType(TableType{TableState::Sealed, TypeLevel{}, globalScope});
getMutable<TableType>(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<string>)
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<string>)
rx.x = "hello!" -- error
end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
}
TEST_SUITE_END();

View file

@ -360,7 +360,7 @@ TEST_CASE_FIXTURE(Fixture, "parametric_tagged_union_alias")
type Result<O, E> = Ok<O> | Err<E> type Result<O, E> = Ok<O> | Err<E>
local a : Result<string, number> = {success = false, result = "hotdogs"} local a : Result<string, number> = {success = false, result = "hotdogs"}
local b : Result<string, number> = {success = true, result = "hotdogs"} -- local b : Result<string, number> = {success = true, result = "hotdogs"}
)"); )");
LUAU_REQUIRE_ERROR_COUNT(1, result); LUAU_REQUIRE_ERROR_COUNT(1, result);

View file

@ -357,7 +357,7 @@ TEST_CASE_FIXTURE(Fixture, "open_table_unification_2")
LUAU_REQUIRE_ERROR_COUNT(1, result); LUAU_REQUIRE_ERROR_COUNT(1, result);
TypeError& err = result.errors[0]; TypeError& err = result.errors[0];
MissingProperties* error = get<MissingProperties>(err); MissingProperties* error = get<MissingProperties>(err);
REQUIRE(error != nullptr); REQUIRE_MESSAGE(error != nullptr, "Expected MissingProperties but got " << toString(err));
REQUIRE(error->properties.size() == 1); REQUIRE(error->properties.size() == 1);
CHECK_EQ("y", error->properties[0]); 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); LUAU_REQUIRE_ERROR_COUNT(1, result);
MissingProperties* error = get<MissingProperties>(result.errors[0]); MissingProperties* error = get<MissingProperties>(result.errors[0]);
REQUIRE(error != nullptr); REQUIRE_MESSAGE(error != nullptr, "Expected MissingProperties but got " << toString(result.errors[0]));
REQUIRE(error->properties.size() == 1); REQUIRE(error->properties.size() == 1);
CHECK_EQ("baz", error->properties[0]); 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); LUAU_REQUIRE_ERROR_COUNT(1, result);
TypeError& err = result.errors[0]; TypeError& err = result.errors[0];
MissingProperties* error = get<MissingProperties>(err); MissingProperties* error = get<MissingProperties>(err);
REQUIRE(error != nullptr); REQUIRE_MESSAGE(error != nullptr, "Expected MissingProperties but got " << toString(err));
REQUIRE(error->properties.size() == 1); REQUIRE(error->properties.size() == 1);
CHECK_EQ("baz", error->properties[0]); 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}})); 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<TableType>(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<TableType>(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") TEST_CASE_FIXTURE(Fixture, "table_unification_4")
{ {
CheckResult result = check(R"( CheckResult result = check(R"(
@ -680,7 +634,8 @@ TEST_CASE_FIXTURE(Fixture, "indexers_get_quantified_too")
REQUIRE("number" == toString(indexer.indexType)); REQUIRE("number" == toString(indexer.indexType));
REQUIRE(nullptr != get<GenericType>(follow(indexer.indexResultType))); TypeId indexResultType = follow(indexer.indexResultType);
REQUIRE_MESSAGE(get<GenericType>(indexResultType), "Expected generic but got " << toString(indexResultType));
} }
TEST_CASE_FIXTURE(Fixture, "indexers_quantification_2") 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") TEST_CASE_FIXTURE(BuiltinsFixture, "meta_add_both_ways")
{ {
ScopedFastFlag sff{FFlag::DebugLuauDeferredConstraintResolution, false};
CheckResult result = check(R"( CheckResult result = check(R"(
type VectorMt = { __add: (Vector, number) -> Vector } type VectorMt = { __add: (Vector, number) -> Vector }
local vectorMt: VectorMt local vectorMt: VectorMt
@ -1093,6 +1050,30 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "meta_add_both_ways")
CHECK_EQ(*requireType("a"), *requireType("c")); 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 // 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. // As a result, type inference crashed with a stack overflow.
TEST_CASE_FIXTURE(BuiltinsFixture, "unification_of_unions_in_a_self_referential_type") 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); LUAU_REQUIRE_ERROR_COUNT(1, result);
MissingProperties* mp = get<MissingProperties>(result.errors[0]); MissingProperties* mp = get<MissingProperties>(result.errors[0]);
REQUIRE(mp); REQUIRE_MESSAGE(mp, "Expected MissingProperties but got " << toString(result.errors[0]));
CHECK_EQ(mp->context, MissingProperties::Missing); CHECK_EQ(mp->context, MissingProperties::Missing);
REQUIRE_EQ(1, mp->properties.size()); REQUIRE_EQ(1, mp->properties.size());
CHECK_EQ(mp->properties[0], "a"); 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) if (FFlag::DebugLuauDeferredConstraintResolution)
{ {
LUAU_REQUIRE_ERROR_COUNT(1, result); 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 else
{ {
@ -1685,7 +1666,7 @@ TEST_CASE_FIXTURE(Fixture, "table_subtyping_with_missing_props_dont_report_multi
if (FFlag::DebugLuauDeferredConstraintResolution) 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 }", " at [0], { x: number } is not a subtype of { x: number, y: number, z: number }",
toString(result.errors[0])); toString(result.errors[0]));
} }
@ -2176,7 +2157,7 @@ local b: B = a
LUAU_REQUIRE_ERRORS(result); LUAU_REQUIRE_ERRORS(result);
if (FFlag::DebugLuauDeferredConstraintResolution) 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 else
{ {
const std::string expected = R"(Type 'A' could not be converted into 'B' 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); LUAU_REQUIRE_ERRORS(result);
if (FFlag::DebugLuauDeferredConstraintResolution) 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 else
{ {
const std::string expected = R"(Type 'A' could not be converted into 'B' 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); LUAU_REQUIRE_ERROR_COUNT(1, result);
std::string expected = "Type 'a' could not be converted into 'T'; at [\"a\"], string is not exactly number" std::string expected = "Type 'a' could not be converted into 'T'; at [read \"a\"], string is not exactly number"
"\n\tat [\"b\"], boolean is not exactly string" "\n\tat [read \"b\"], boolean is not exactly string"
"\n\tat [\"c\"], number is not exactly boolean"; "\n\tat [read \"c\"], number is not exactly boolean";
CHECK(toString(result.errors[0]) == expected); CHECK(toString(result.errors[0]) == expected);
} }
TEST_CASE_FIXTURE(Fixture, "read_and_write_only_table_properties_are_unsupported") 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"( CheckResult result = check(R"(
type W = {read x: number} 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); 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") TEST_CASE_FIXTURE(Fixture, "table_subtyping_error_suppression")
{ {
CheckResult result = check(R"( 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<PropertyAccessViolation>(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("<a, b, c...>({{ read Character: a }}, { Character: t1 }) -> () "
"where "
"t1 = a & { read FindFirstChild: (t1, string) -> (b, c...) }" == toString(requireType("oc")));
// We currently get
// <a, b, c...>({{ read Character: a }}, { Character: t1 }) -> () where t1 = { read FindFirstChild: (t1, string) -> (b, c...) }
// But we'd like to see
// <a, b...>({{ 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(); TEST_SUITE_END();

View file

@ -15,8 +15,19 @@
using namespace Luau; using namespace Luau;
using namespace Luau::TypePath; using namespace Luau::TypePath;
LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution);
LUAU_DYNAMIC_FASTINT(LuauTypePathMaximumTraverseSteps); 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_SUITE_BEGIN("TypePathManipulation");
TEST_CASE("append") TEST_CASE("append")
@ -95,12 +106,12 @@ TEST_SUITE_BEGIN("TypePathTraversal");
LUAU_REQUIRE_NO_ERRORS(result); \ LUAU_REQUIRE_NO_ERRORS(result); \
} while (false); } while (false);
TEST_CASE_FIXTURE(Fixture, "empty_traversal") TEST_CASE_FIXTURE(TypePathFixture, "empty_traversal")
{ {
CHECK(traverseForType(builtinTypes->numberType, kEmpty, builtinTypes) == builtinTypes->numberType); CHECK(traverseForType(builtinTypes->numberType, kEmpty, builtinTypes) == builtinTypes->numberType);
} }
TEST_CASE_FIXTURE(Fixture, "table_property") TEST_CASE_FIXTURE(TypePathFixture, "table_property")
{ {
TYPESOLVE_CODE(R"( TYPESOLVE_CODE(R"(
local x = { y = 123 } 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); 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") 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") SUBCASE("unions")
{ {
@ -242,7 +253,7 @@ TEST_CASE_FIXTURE(ClassFixture, "metatables")
} }
} }
TEST_CASE_FIXTURE(Fixture, "bounds") TEST_CASE_FIXTURE(TypePathFixture, "bounds")
{ {
SUBCASE("free_type") SUBCASE("free_type")
{ {
@ -274,7 +285,7 @@ TEST_CASE_FIXTURE(Fixture, "bounds")
} }
} }
TEST_CASE_FIXTURE(Fixture, "indexers") TEST_CASE_FIXTURE(TypePathFixture, "indexers")
{ {
SUBCASE("table") SUBCASE("table")
{ {
@ -308,7 +319,7 @@ TEST_CASE_FIXTURE(Fixture, "indexers")
// TODO: Class types // TODO: Class types
} }
TEST_CASE_FIXTURE(Fixture, "negated") TEST_CASE_FIXTURE(TypePathFixture, "negated")
{ {
SUBCASE("valid") SUBCASE("valid")
{ {
@ -327,7 +338,7 @@ TEST_CASE_FIXTURE(Fixture, "negated")
} }
} }
TEST_CASE_FIXTURE(Fixture, "variadic") TEST_CASE_FIXTURE(TypePathFixture, "variadic")
{ {
SUBCASE("valid") SUBCASE("valid")
{ {
@ -346,7 +357,7 @@ TEST_CASE_FIXTURE(Fixture, "variadic")
} }
} }
TEST_CASE_FIXTURE(Fixture, "arguments") TEST_CASE_FIXTURE(TypePathFixture, "arguments")
{ {
SUBCASE("function") SUBCASE("function")
{ {
@ -368,7 +379,7 @@ TEST_CASE_FIXTURE(Fixture, "arguments")
} }
} }
TEST_CASE_FIXTURE(Fixture, "returns") TEST_CASE_FIXTURE(TypePathFixture, "returns")
{ {
SUBCASE("function") SUBCASE("function")
{ {
@ -391,7 +402,7 @@ TEST_CASE_FIXTURE(Fixture, "returns")
} }
} }
TEST_CASE_FIXTURE(Fixture, "tail") TEST_CASE_FIXTURE(TypePathFixture, "tail")
{ {
SUBCASE("has_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 // This will fail an occurs check, but it's a quick example of a cyclic type
// where there _is_ no traversal. // 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); ScopedFastInt sfi(DFInt::LuauTypePathMaximumTraverseSteps, 2);
@ -466,12 +477,12 @@ TEST_CASE_FIXTURE(Fixture, "step_limit")
)"); )");
TypeId root = requireTypeAlias("T"); 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); auto result = traverseForType(root, path, builtinTypes);
CHECK(!result); CHECK(!result);
} }
TEST_CASE_FIXTURE(BuiltinsFixture, "complex_chains") TEST_CASE_FIXTURE(TypePathBuiltinsFixture, "complex_chains")
{ {
SUBCASE("add_metamethod_return_type") SUBCASE("add_metamethod_return_type")
{ {
@ -484,7 +495,7 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "complex_chains")
)"); )");
TypeId root = requireTypeAlias("Tab"); 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); auto result = traverseForType(root, path, builtinTypes);
CHECK(result == builtinTypes->numberType); CHECK(result == builtinTypes->numberType);
} }
@ -498,7 +509,7 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "complex_chains")
)"); )");
TypeId root = requireTypeAlias("Obj"); 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); auto result = traverseForType(root, path, builtinTypes);
CHECK(*result == builtinTypes->falseType); CHECK(*result == builtinTypes->falseType);
} }
@ -510,6 +521,10 @@ TEST_SUITE_BEGIN("TypePathToString");
TEST_CASE("field") TEST_CASE("field")
{ {
ScopedFastFlag sff[] = {
{FFlag::DebugLuauDeferredConstraintResolution, false},
};
CHECK(toString(PathBuilder().prop("foo").build()) == R"(["foo"])"); CHECK(toString(PathBuilder().prop("foo").build()) == R"(["foo"])");
} }
@ -535,10 +550,26 @@ TEST_CASE("empty_path")
TEST_CASE("prop") TEST_CASE("prop")
{ {
ScopedFastFlag sff[] = {
{FFlag::DebugLuauDeferredConstraintResolution, false},
};
Path p = PathBuilder().prop("foo").build(); Path p = PathBuilder().prop("foo").build();
CHECK(p == Path(TypePath::Property{"foo"})); 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") TEST_CASE("index")
{ {
Path p = PathBuilder().index(0).build(); Path p = PathBuilder().index(0).build();
@ -561,8 +592,10 @@ TEST_CASE("fields")
TEST_CASE("chained") TEST_CASE("chained")
{ {
CHECK(PathBuilder().index(0).prop("foo").mt().prop("bar").args().index(1).build() == ScopedFastFlag sff{FFlag::DebugLuauDeferredConstraintResolution, true};
Path({Index{0}, TypePath::Property{"foo"}, TypeField::Metatable, TypePath::Property{"bar"}, PackField::Arguments, Index{1}}));
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 TEST_SUITE_END(); // TypePathBuilder

View file

@ -76,13 +76,13 @@ TEST_CASE_FIXTURE(Unifier2Fixture, "T <: U")
CHECK(u2.unify(left, right)); CHECK(u2.unify(left, right));
CHECK("t1 where t1 = ('a <: (t1 <: 'b))" == toString(left)); CHECK("'a" == toString(left));
CHECK("t1 where t1 = (('a <: t1) <: 'b)" == toString(right)); CHECK("'a" == toString(right));
CHECK("never" == toString(freeLeft->lowerBound)); 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)); CHECK("unknown" == toString(freeRight->upperBound));
} }

View file

@ -293,6 +293,24 @@ end
assert(loopIteratorProtocol(0, table.create(100, 5)) == 5058) 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) local function vec3compsum(a: vector)
return a.X + a.Y + a.Z return a.X + a.Y + a.Z
end end

View file

@ -1,4 +1,3 @@
AnnotationTests.typeof_expr
AstQuery.last_argument_function_call_type AstQuery.last_argument_function_call_type
AutocompleteTest.anonymous_autofilled_generic_on_argument_type_pack_vararg AutocompleteTest.anonymous_autofilled_generic_on_argument_type_pack_vararg
AutocompleteTest.anonymous_autofilled_generic_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.assert_returns_false_and_string_iff_it_knows_the_first_argument_cannot_be_truthy
BuiltinTests.bad_select_should_not_crash BuiltinTests.bad_select_should_not_crash
BuiltinTests.coroutine_resume_anything_goes BuiltinTests.coroutine_resume_anything_goes
BuiltinTests.debug_info_is_crazy
BuiltinTests.global_singleton_types_are_sealed BuiltinTests.global_singleton_types_are_sealed
BuiltinTests.gmatch_capture_types BuiltinTests.gmatch_capture_types
BuiltinTests.gmatch_capture_types2 BuiltinTests.gmatch_capture_types2
@ -67,24 +65,12 @@ DefinitionTests.class_definition_string_props
DefinitionTests.declaring_generic_functions DefinitionTests.declaring_generic_functions
DefinitionTests.definition_file_classes DefinitionTests.definition_file_classes
Differ.equal_generictp_cyclic 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
Differ.generictp_normal_2 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_left
Differ.metatable_metamissing_right Differ.metatable_metamissing_right
Differ.metatable_metanormal Differ.metatable_metanormal
Differ.negation 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.accumulate_cached_errors_in_consistent_order
FrontendTest.environments FrontendTest.environments
FrontendTest.imported_table_modification_2 FrontendTest.imported_table_modification_2
@ -94,47 +80,37 @@ FrontendTest.trace_requires_in_nonstrict_mode
GenericsTests.apply_type_function_nested_generics1 GenericsTests.apply_type_function_nested_generics1
GenericsTests.better_mismatch_error_messages GenericsTests.better_mismatch_error_messages
GenericsTests.bound_tables_do_not_clone_original_fields 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.correctly_instantiate_polymorphic_member_functions
GenericsTests.do_not_always_instantiate_generic_intersection_types 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.dont_substitute_bound_types
GenericsTests.error_detailed_function_mismatch_generic_pack GenericsTests.error_detailed_function_mismatch_generic_pack
GenericsTests.error_detailed_function_mismatch_generic_types GenericsTests.error_detailed_function_mismatch_generic_types
GenericsTests.factories_of_generics GenericsTests.factories_of_generics
GenericsTests.function_arguments_can_be_polytypes
GenericsTests.generic_argument_count_too_few GenericsTests.generic_argument_count_too_few
GenericsTests.generic_argument_count_too_many GenericsTests.generic_argument_count_too_many
GenericsTests.generic_factories GenericsTests.generic_factories
GenericsTests.generic_functions_dont_cache_type_parameters GenericsTests.generic_functions_dont_cache_type_parameters
GenericsTests.generic_functions_in_types GenericsTests.generic_functions_in_types
GenericsTests.generic_type_families_work_in_subtyping
GenericsTests.generic_type_pack_parentheses GenericsTests.generic_type_pack_parentheses
GenericsTests.generic_type_pack_unification1 GenericsTests.generic_type_pack_unification1
GenericsTests.generic_type_pack_unification2 GenericsTests.generic_type_pack_unification2
GenericsTests.generic_type_pack_unification3 GenericsTests.generic_type_pack_unification3
GenericsTests.higher_rank_polymorphism_should_not_accept_instantiated_arguments GenericsTests.higher_rank_polymorphism_should_not_accept_instantiated_arguments
GenericsTests.hof_subtype_instantiation_regression 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_2
GenericsTests.infer_generic_function_function_argument_3 GenericsTests.infer_generic_function_function_argument_3
GenericsTests.infer_generic_function_function_argument_overloaded GenericsTests.infer_generic_function_function_argument_overloaded
GenericsTests.infer_generic_lib_function_function_argument 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.instantiated_function_argument_names
GenericsTests.local_vars_can_be_polytypes GenericsTests.mutable_state_polymorphism
GenericsTests.no_stack_overflow_from_quantifying GenericsTests.no_stack_overflow_from_quantifying
GenericsTests.properties_can_be_instantiated_polytypes GenericsTests.properties_can_be_instantiated_polytypes
GenericsTests.properties_can_be_polytypes
GenericsTests.quantify_functions_even_if_they_have_an_explicit_generic GenericsTests.quantify_functions_even_if_they_have_an_explicit_generic
GenericsTests.rank_N_types_via_typeof
GenericsTests.self_recursive_instantiated_param GenericsTests.self_recursive_instantiated_param
GenericsTests.type_parameters_can_be_polytypes GenericsTests.type_parameters_can_be_polytypes
GenericsTests.typefuns_sharing_types GenericsTests.typefuns_sharing_types
@ -164,7 +140,6 @@ IntersectionTypes.overloadeded_functions_with_weird_typepacks_3
IntersectionTypes.overloadeded_functions_with_weird_typepacks_4 IntersectionTypes.overloadeded_functions_with_weird_typepacks_4
IntersectionTypes.table_write_sealed_indirect IntersectionTypes.table_write_sealed_indirect
IntersectionTypes.union_saturate_overloaded_functions IntersectionTypes.union_saturate_overloaded_functions
Linter.DeprecatedApiFenv
Linter.FormatStringTyped Linter.FormatStringTyped
Linter.TableOperationsIndexer Linter.TableOperationsIndexer
ModuleTests.clone_self_property ModuleTests.clone_self_property
@ -210,16 +185,12 @@ RefinementTest.discriminate_from_isa_of_x
RefinementTest.discriminate_from_truthiness_of_x RefinementTest.discriminate_from_truthiness_of_x
RefinementTest.discriminate_tag RefinementTest.discriminate_tag
RefinementTest.discriminate_tag_with_implicit_else 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.fail_to_refine_a_property_of_subscript_expression
RefinementTest.falsiness_of_TruthyPredicate_narrows_into_nil 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.globals_can_be_narrowed_too
RefinementTest.impossible_type_narrow_is_not_an_error RefinementTest.impossible_type_narrow_is_not_an_error
RefinementTest.index_on_a_refined_property RefinementTest.index_on_a_refined_property
RefinementTest.isa_type_refinement_must_be_known_ahead_of_time 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.narrow_property_of_a_bounded_variable
RefinementTest.nonoptional_type_can_narrow_to_nil_if_sense_is_true RefinementTest.nonoptional_type_can_narrow_to_nil_if_sense_is_true
RefinementTest.not_t_or_some_prop_of_t 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.array_factory_function
TableTests.casting_tables_with_props_into_table_with_indexer2 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_indexer3
TableTests.casting_tables_with_props_into_table_with_indexer4
TableTests.casting_unsealed_tables_with_props_into_table_with_indexer TableTests.casting_unsealed_tables_with_props_into_table_with_indexer
TableTests.checked_prop_too_early TableTests.checked_prop_too_early
TableTests.cli_84607_missing_prop_in_array_or_dict 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_call_tail
TableTests.common_table_element_union_in_prop TableTests.common_table_element_union_in_prop
TableTests.confusing_indexing TableTests.confusing_indexing
TableTests.cyclic_shifted_tables
TableTests.disallow_indexing_into_an_unsealed_table_with_no_indexer_in_strict_mode 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_crash_when_setmetatable_does_not_produce_a_metatabletypevar
TableTests.dont_extend_unsealed_tables_in_rvalue_position TableTests.dont_extend_unsealed_tables_in_rvalue_position
TableTests.dont_leak_free_table_props TableTests.dont_leak_free_table_props
TableTests.dont_quantify_table_that_belongs_to_outer_scope
TableTests.dont_suggest_exact_match_keys TableTests.dont_suggest_exact_match_keys
TableTests.error_detailed_indexer_key TableTests.error_detailed_indexer_key
TableTests.error_detailed_indexer_value TableTests.error_detailed_indexer_value
@ -275,8 +243,8 @@ TableTests.generalize_table_argument
TableTests.generic_table_instantiation_potential_regression TableTests.generic_table_instantiation_potential_regression
TableTests.indexer_mismatch TableTests.indexer_mismatch
TableTests.indexers_get_quantified_too TableTests.indexers_get_quantified_too
TableTests.indexing_from_a_table_should_prefer_properties_when_possible
TableTests.inequality_operators_imply_exactly_matching_types TableTests.inequality_operators_imply_exactly_matching_types
TableTests.infer_indexer_from_array_like_table
TableTests.infer_indexer_from_its_variable_type_and_unifiable TableTests.infer_indexer_from_its_variable_type_and_unifiable
TableTests.inferred_return_type_of_free_table TableTests.inferred_return_type_of_free_table
TableTests.instantiate_table_cloning_3 TableTests.instantiate_table_cloning_3
@ -286,7 +254,7 @@ TableTests.length_operator_intersection
TableTests.length_operator_non_table_union TableTests.length_operator_non_table_union
TableTests.length_operator_union TableTests.length_operator_union
TableTests.less_exponential_blowup_please TableTests.less_exponential_blowup_please
TableTests.meta_add_both_ways TableTests.meta_add
TableTests.meta_add_inferred TableTests.meta_add_inferred
TableTests.metatable_mismatch_should_fail TableTests.metatable_mismatch_should_fail
TableTests.missing_metatable_for_sealed_tables_do_not_get_inferred TableTests.missing_metatable_for_sealed_tables_do_not_get_inferred
@ -300,8 +268,6 @@ TableTests.oop_polymorphic
TableTests.open_table_unification_2 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
TableTests.pass_a_union_of_tables_to_a_function_that_requires_a_table_2 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.persistent_sealed_table_is_immutable
TableTests.prop_access_on_key_whose_types_mismatches TableTests.prop_access_on_key_whose_types_mismatches
TableTests.prop_access_on_unions_of_indexers_where_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.shared_selfs_through_metatables
TableTests.table_call_metamethod_basic TableTests.table_call_metamethod_basic
TableTests.table_call_metamethod_must_be_callable 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_2
TableTests.table_param_width_subtyping_3 TableTests.table_param_width_subtyping_3
TableTests.table_simple_call TableTests.table_simple_call
TableTests.table_subtyping_with_extra_props_dont_report_multiple_errors 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_subtyping_with_missing_props_dont_report_multiple_errors2
TableTests.table_unification_4 TableTests.table_unification_4
TableTests.table_unifies_into_map 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.result_of_failed_typepack_unification_is_constrained
TryUnifyTests.uninhabited_table_sub_anything TryUnifyTests.uninhabited_table_sub_anything
TryUnifyTests.uninhabited_table_sub_never TryUnifyTests.uninhabited_table_sub_never
TryUnifyTests.variadics_should_use_reversed_properly
TypeAliases.dont_lose_track_of_PendingExpansionTypes_after_substitution TypeAliases.dont_lose_track_of_PendingExpansionTypes_after_substitution
TypeAliases.generic_param_remap TypeAliases.generic_param_remap
TypeAliases.mismatched_generic_type_param TypeAliases.mismatched_generic_type_param
@ -376,10 +339,10 @@ TypeFamilyTests.internal_families_raise_errors
TypeFamilyTests.table_internal_families TypeFamilyTests.table_internal_families
TypeFamilyTests.type_families_inhabited_with_normalization TypeFamilyTests.type_families_inhabited_with_normalization
TypeFamilyTests.unsolvable_family TypeFamilyTests.unsolvable_family
TypeInfer.be_sure_to_use_active_txnlog_when_evaluating_a_variadic_overload
TypeInfer.bidirectional_checking_of_callback_property TypeInfer.bidirectional_checking_of_callback_property
TypeInfer.check_type_infer_recursion_count TypeInfer.check_type_infer_recursion_count
TypeInfer.checking_should_not_ice TypeInfer.checking_should_not_ice
TypeInfer.cli_39932_use_unifier_in_ensure_methods
TypeInfer.cli_50041_committing_txnlog_in_apollo_client_error TypeInfer.cli_50041_committing_txnlog_in_apollo_client_error
TypeInfer.dont_ice_when_failing_the_occurs_check TypeInfer.dont_ice_when_failing_the_occurs_check
TypeInfer.dont_report_type_errors_within_an_AstExprError 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_is_supertype_of_concrete_functions
TypeInferFunctions.function_statement_sealed_table_assignment_through_indexer TypeInferFunctions.function_statement_sealed_table_assignment_through_indexer
TypeInferFunctions.generic_packs_are_not_variadic TypeInferFunctions.generic_packs_are_not_variadic
TypeInferFunctions.higher_order_function_2
TypeInferFunctions.higher_order_function_4 TypeInferFunctions.higher_order_function_4
TypeInferFunctions.improved_function_arg_mismatch_error_nonstrict TypeInferFunctions.improved_function_arg_mismatch_error_nonstrict
TypeInferFunctions.improved_function_arg_mismatch_errors 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
TypeInferFunctions.param_1_and_2_both_takes_the_same_generic_but_their_arguments_are_incompatible_2 TypeInferFunctions.param_1_and_2_both_takes_the_same_generic_but_their_arguments_are_incompatible_2
TypeInferFunctions.record_matching_overload TypeInferFunctions.record_matching_overload
TypeInferFunctions.regex_benchmark_string_format_minimization
TypeInferFunctions.report_exiting_without_return_nonstrict TypeInferFunctions.report_exiting_without_return_nonstrict
TypeInferFunctions.return_type_by_overload TypeInferFunctions.return_type_by_overload
TypeInferFunctions.too_few_arguments_variadic 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_incompatible_args_to_iterator
TypeInferLoops.for_in_loop_with_next TypeInferLoops.for_in_loop_with_next
TypeInferLoops.for_in_with_an_iterator_of_type_any 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.for_loop
TypeInferLoops.ipairs_produces_integral_indices TypeInferLoops.ipairs_produces_integral_indices
TypeInferLoops.iterate_over_free_table TypeInferLoops.iterate_over_free_table
@ -511,7 +470,6 @@ TypeInferLoops.properly_infer_iteratee_is_a_free_table
TypeInferLoops.repeat_loop TypeInferLoops.repeat_loop
TypeInferLoops.varlist_declared_by_for_in_loop_should_be_free TypeInferLoops.varlist_declared_by_for_in_loop_should_be_free
TypeInferLoops.while_loop TypeInferLoops.while_loop
TypeInferModules.bound_free_table_export_is_ok
TypeInferModules.custom_require_global TypeInferModules.custom_require_global
TypeInferModules.do_not_modify_imported_types TypeInferModules.do_not_modify_imported_types
TypeInferModules.do_not_modify_imported_types_5 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_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_it_wont_help_2
TypeInferOOP.dont_suggest_using_colon_rather_than_dot_if_not_defined_with_colon 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.inferring_hundreds_of_self_calls_should_not_suffocate_memory
TypeInferOOP.methods_are_topologically_sorted TypeInferOOP.methods_are_topologically_sorted
TypeInferOOP.object_constructor_can_refer_to_method_of_self 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
TypeInferOperators.typecheck_overloaded_multiply_that_is_an_intersection_on_rhs TypeInferOperators.typecheck_overloaded_multiply_that_is_an_intersection_on_rhs
TypeInferOperators.typecheck_unary_len_error TypeInferOperators.typecheck_unary_len_error
TypeInferOperators.typecheck_unary_minus
TypeInferOperators.typecheck_unary_minus_error TypeInferOperators.typecheck_unary_minus_error
TypeInferOperators.UnknownGlobalCompoundAssign TypeInferOperators.UnknownGlobalCompoundAssign
TypeInferPrimitives.CheckMethodsOfNumber TypeInferPrimitives.CheckMethodsOfNumber
@ -564,7 +520,6 @@ TypePackTests.pack_tail_unification_check
TypePackTests.type_alias_backwards_compatible TypePackTests.type_alias_backwards_compatible
TypePackTests.type_alias_default_type_errors TypePackTests.type_alias_default_type_errors
TypePackTests.type_alias_type_packs_import TypePackTests.type_alias_type_packs_import
TypePackTests.type_packs_with_tails_in_vararg_adjustment
TypePackTests.unify_variadic_tails_in_arguments TypePackTests.unify_variadic_tails_in_arguments
TypeSingletons.enums_using_singletons_mismatch TypeSingletons.enums_using_singletons_mismatch
TypeSingletons.error_detailed_tagged_union_mismatch_bool TypeSingletons.error_detailed_tagged_union_mismatch_bool

View file

@ -385,7 +385,7 @@ def luau_typepath_property_summary(valobj, internal_dict, options):
read_write = False read_write = False
try: try:
fflag_valobj = valobj.GetFrame().GetValueForVariablePath( fflag_valobj = valobj.GetFrame().GetValueForVariablePath(
"FFlag::DebugLuauReadWriteProperties::value") "FFlag::DebugLuauDeferredConstraintResolution::value")
read_write = fflag_valobj.GetValue() == "true" read_write = fflag_valobj.GetValue() == "true"
except Exception as e: except Exception as e:

View file

@ -107,12 +107,6 @@ def main():
action="store_true", action="store_true",
help="Write a new faillist.txt after running tests.", 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( parser.add_argument(
"--ts", "--ts",
dest="suite", dest="suite",
@ -135,8 +129,6 @@ def main():
failList = loadFailList() failList = loadFailList()
flags = ["true", "DebugLuauDeferredConstraintResolution"] flags = ["true", "DebugLuauDeferredConstraintResolution"]
if args.rwp:
flags.append("DebugLuauReadWriteProperties")
commandLine = [args.path, "--reporters=xml", "--fflags=" + ",".join(flags)] commandLine = [args.path, "--reporters=xml", "--fflags=" + ",".join(flags)]