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

View file

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

View file

@ -8,6 +8,7 @@
#include "Luau/Location.h"
#include "Luau/Module.h"
#include "Luau/Normalize.h"
#include "Luau/Substitution.h"
#include "Luau/ToString.h"
#include "Luau/Type.h"
#include "Luau/TypeCheckLimits.h"
@ -20,6 +21,8 @@
namespace Luau
{
enum class ValueContext;
struct DcrLogger;
// TypeId, TypePackId, or Constraint*. It is impossible to know which, but we
@ -110,8 +113,6 @@ struct ConstraintSolver
bool isDone();
void finalizeModule();
/** Attempt to dispatch a constraint. Returns true if it was successful. If
* tryDispatch() returns false, the constraint remains in the unsolved set
* and will be retried later.
@ -136,6 +137,7 @@ struct ConstraintSolver
bool tryDispatch(const SetOpConstraint& c, NotNull<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 EqualityConstraint& c, NotNull<const Constraint> constraint, bool force);
// for a, ... in some_table do
// also handles __iter metamethod
@ -146,9 +148,9 @@ struct ConstraintSolver
TypeId nextTy, TypeId tableTy, TypeId firstIndexTy, const IterableConstraint& c, NotNull<const Constraint> constraint, bool force);
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(
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);
/**
@ -208,23 +210,6 @@ struct ConstraintSolver
*/
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.
* @param cv the body of the constraint.
**/
@ -253,20 +238,33 @@ struct ConstraintSolver
*/
bool hasUnresolvedConstraints(TypeId ty);
private:
/** Helper used by tryDispatch(SubtypeConstraint) and
* tryDispatch(PackSubtypeConstraint)
/**
* Creates a new Unifier and performs a single unification operation.
*
* Attempts to unify subTy with superTy. If doing so would require unifying
* @param subType the sub-type to unify.
* @param superType the super-type to unify.
* @returns true if the unification succeeded. False if the unification was
* too complex.
*/
template <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.
*
* Note: TID can only be TypeId or TypePackId.
*
* If unification fails, replace all free types with errorType.
*
* If unification succeeds, unblock every type changed by the unification.
*
* @returns true if the unification succeeded. False if the unification was
* too complex.
*/
template<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
* itself in the case that resultTy == blockedTy. This can happen if we
@ -295,6 +293,13 @@ private:
**/
void unblock_(BlockedConstraintId progressed);
/**
* Reproduces any constraints necessary for new types that are copied when applying a substitution.
* At the time of writing, this pertains only to type families.
* @param subst the substitution that was applied
**/
void reproduceConstraints(NotNull<Scope> scope, const Location& location, const Substitution& subst);
TypeId errorRecoveryType() const;
TypePackId errorRecoveryTypePack() const;

View file

@ -380,6 +380,20 @@ struct NonStrictFunctionDefinitionError
bool operator==(const NonStrictFunctionDefinitionError& rhs) const;
};
struct PropertyAccessViolation
{
TypeId table;
Name key;
enum
{
CannotRead,
CannotWrite
} context;
bool operator==(const PropertyAccessViolation& rhs) const;
};
struct CheckedFunctionIncorrectArgs
{
std::string functionName;
@ -388,14 +402,28 @@ struct CheckedFunctionIncorrectArgs
bool operator==(const CheckedFunctionIncorrectArgs& rhs) const;
};
using TypeErrorData =
Variant<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, CheckedFunctionIncorrectArgs>;
struct UnexpectedTypeInSubtyping
{
TypeId ty;
bool operator==(const UnexpectedTypeInSubtyping& rhs) const;
};
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
{

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 Normalizer;
struct NormalizedType;
struct NormalizedClassType;
struct NormalizedStringType;
struct NormalizedFunctionType;
struct TypeArena;
struct TypeCheckLimits;
struct NormalizedStringType;
struct NormalizedType;
struct Property;
struct Scope;
struct TableIndexer;
struct TypeArena;
struct TypeCheckLimits;
enum class SubtypingVariance
{
@ -79,6 +80,7 @@ struct SubtypingResult
SubtypingResult& withSubPath(TypePath::Path path);
SubtypingResult& withSuperPath(TypePath::Path path);
SubtypingResult& withErrors(ErrorVec& err);
SubtypingResult& withError(TypeError err);
// Only negates the `isSubtype`.
static SubtypingResult negate(const SubtypingResult& result);
@ -102,6 +104,10 @@ struct SubtypingEnvironment
DenseHashMap<TypePackId, TypePackId> mappedGenericPacks{nullptr};
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
@ -192,6 +198,7 @@ private:
SubtypingResult isCovariantWith(SubtypingEnvironment& env, const SingletonType* subSingleton, const TableType* superTable);
SubtypingResult isCovariantWith(SubtypingEnvironment& env, const TableIndexer& subIndexer, const TableIndexer& superIndexer);
SubtypingResult isCovariantWith(SubtypingEnvironment& env, const Property& subProperty, const Property& superProperty, const std::string& name);
SubtypingResult isCovariantWith(SubtypingEnvironment& env, const NormalizedType* subNorm, const NormalizedType* superNorm);
SubtypingResult isCovariantWith(SubtypingEnvironment& env, const NormalizedClassType& subClass, const NormalizedClassType& superClass);

View file

@ -414,16 +414,11 @@ struct Property
TypeId type() const;
void setType(TypeId ty);
// Should only be called in RWP!
// We do not assert that `readTy` nor `writeTy` are nullopt or not.
// The invariant is that at least one of them mustn't be nullopt, which we do assert here.
// TODO: Kill this in favor of exposing `readTy`/`writeTy` directly? If we do, we'll lose the asserts which will be useful while debugging.
std::optional<TypeId> readType() const;
std::optional<TypeId> writeType() const;
bool isShared() const;
bool isReadOnly() const;
bool isWriteOnly() const;
bool isReadWrite() const;
private:
std::optional<TypeId> readTy;
std::optional<TypeId> writeTy;
};
@ -844,6 +839,7 @@ public:
const TypePackId emptyTypePack;
const TypePackId anyTypePack;
const TypePackId unknownTypePack;
const TypePackId neverTypePack;
const TypePackId uninhabitableTypePack;
const TypePackId errorTypePack;

View file

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

View file

@ -28,6 +28,8 @@ std::optional<TypeId> findMetatableEntry(
NotNull<BuiltinTypes> builtinTypes, ErrorVec& errors, TypeId type, const std::string& entry, Location location);
std::optional<TypeId> findTablePropertyRespectingMeta(
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.
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};
// 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 recursionLimit = 0;

View file

@ -11,7 +11,6 @@
LUAU_FASTINT(LuauVisitRecursionLimit)
LUAU_FASTFLAG(LuauBoundLazyTypes2)
LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution)
LUAU_FASTFLAG(DebugLuauReadWriteProperties)
namespace Luau
{
@ -281,12 +280,16 @@ struct GenericTypeVisitor
{
for (auto& [_name, prop] : ttv->props)
{
if (FFlag::DebugLuauReadWriteProperties)
if (FFlag::DebugLuauDeferredConstraintResolution)
{
if (auto ty = prop.readType())
if (auto ty = prop.readTy)
traverse(*ty);
if (auto ty = prop.writeType())
// In the case that the readType and the writeType
// are the same pointer, just traverse once.
// Traversing each property twice has pretty
// significant performance consequences.
if (auto ty = prop.writeTy; ty && !prop.isShared())
traverse(*ty);
}
else
@ -315,12 +318,16 @@ struct GenericTypeVisitor
{
for (const auto& [name, prop] : ctv->props)
{
if (FFlag::DebugLuauReadWriteProperties)
if (FFlag::DebugLuauDeferredConstraintResolution)
{
if (auto ty = prop.readType())
if (auto ty = prop.readTy)
traverse(*ty);
if (auto ty = prop.writeType())
// In the case that the readType and the writeType are
// the same pointer, just traverse once. Traversing each
// property twice would have pretty significant
// performance consequences.
if (auto ty = prop.writeTy; ty && !prop.isShared())
traverse(*ty);
}
else

View file

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

View file

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

View file

@ -283,6 +283,26 @@ void registerBuiltinGlobals(Frontend& frontend, GlobalTypes& globals, bool typeC
}
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, "select"), magicFunctionSelect);
attachDcrMagicFunction(getGlobalBinding(globals, "select"), dcrMagicFunctionSelect);

View file

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

View file

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

View file

@ -6,6 +6,7 @@
#include "Luau/ConstraintSolver.h"
#include "Luau/DcrLogger.h"
#include "Luau/Instantiation.h"
#include "Luau/Instantiation2.h"
#include "Luau/Location.h"
#include "Luau/ModuleResolver.h"
#include "Luau/OverloadResolution.h"
@ -430,8 +431,6 @@ void ConstraintSolver::run()
progress |= runSolverPass(true);
} while (progress);
finalizeModule();
if (FFlag::DebugLuauLogSolver)
{
dumpBindings(rootScope, opts);
@ -477,48 +476,6 @@ struct FreeTypeSearcher : TypeOnceVisitor
} // namespace
void ConstraintSolver::finalizeModule()
{
Anyification a{arena, rootScope, builtinTypes, &iceReporter, builtinTypes->anyType, builtinTypes->anyTypePack};
std::optional<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)
{
if (!force && isBlocked(constraint))
@ -562,6 +519,8 @@ bool ConstraintSolver::tryDispatch(NotNull<const Constraint> constraint, bool fo
success = tryDispatch(*rc, constraint, force);
else if (auto rpc = get<ReducePackConstraint>(*constraint))
success = tryDispatch(*rpc, constraint, force);
else if (auto eqc = get<EqualityConstraint>(*constraint))
success = tryDispatch(*eqc, constraint, force);
else
LUAU_ASSERT(false);
@ -578,7 +537,9 @@ bool ConstraintSolver::tryDispatch(const SubtypeConstraint& c, NotNull<const Con
else if (isBlocked(c.superType))
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)
@ -588,7 +549,9 @@ bool ConstraintSolver::tryDispatch(const PackSubtypeConstraint& c, NotNull<const
else if (isBlocked(c.superPack))
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)
@ -615,13 +578,13 @@ bool ConstraintSolver::tryDispatch(const GeneralizationConstraint& c, NotNull<co
if (get<BlockedType>(generalizedType))
asMutable(generalizedType)->ty.emplace<BoundType>(generalized->result);
else
unify(constraint->scope, constraint->location, generalizedType, generalized->result);
unify(constraint, generalizedType, generalized->result);
for (auto [free, gen] : generalized->insertedGenerics.pairings)
unify(constraint->scope, constraint->location, free, gen);
unify(constraint, free, gen);
for (auto [free, gen] : generalized->insertedGenericPacks.pairings)
unify(constraint->scope, constraint->location, free, gen);
unify(constraint, free, gen);
}
else
{
@ -632,6 +595,9 @@ bool ConstraintSolver::tryDispatch(const GeneralizationConstraint& c, NotNull<co
unblock(c.generalizedType, constraint->location);
unblock(c.sourceType, constraint->location);
for (TypeId ty : c.interiorTypes)
u2.generalize(ty);
return true;
}
@ -730,7 +696,7 @@ bool ConstraintSolver::tryDispatch(const IterableConstraint& c, NotNull<const Co
Anyification anyify{arena, constraint->scope, builtinTypes, &iceReporter, errorRecoveryType(), errorRecoveryTypePack()};
std::optional<TypePackId> anyified = anyify.substitute(c.variables);
LUAU_ASSERT(anyified);
unify(constraint->scope, constraint->location, *anyified, c.variables);
unify(constraint, *anyified, c.variables);
return true;
}
@ -1112,6 +1078,26 @@ bool ConstraintSolver::tryDispatch(const FunctionCallConstraint& c, NotNull<cons
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 (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)
{
const TypeId fn = follow(c.fn);
TypeId fn = follow(c.fn);
const TypePackId argsPack = follow(c.argsPack);
if (isBlocked(fn))
@ -1156,6 +1142,35 @@ bool ConstraintSolver::tryDispatch(const FunctionCheckConstraint& c, NotNull<con
if (!ftv)
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> 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))
return block(subjectType, constraint);
auto [blocked, result] = lookupTableProp(subjectType, c.prop, c.suppressSimplification);
auto [blocked, result] = lookupTableProp(subjectType, c.prop, c.context, c.suppressSimplification);
if (!blocked.empty())
{
for (TypeId blocked : blocked)
@ -1304,7 +1319,7 @@ static void updateTheTableType(
for (size_t i = 0; i < path.size() - 1; ++i)
{
t = follow(t);
auto propTy = findTablePropertyRespectingMeta(builtinTypes, dummy, t, path[i], Location{});
auto propTy = findTablePropertyRespectingMeta(builtinTypes, dummy, t, path[i], ValueContext::LValue, Location{});
dummy.clear();
if (!propTy)
@ -1329,17 +1344,26 @@ static void updateTheTableType(
bool ConstraintSolver::tryDispatch(const SetPropConstraint& c, NotNull<const Constraint> constraint, bool force)
{
TypeId subjectType = follow(c.subjectType);
const TypeId propType = follow(c.propType);
if (isBlocked(subjectType))
return block(subjectType, constraint);
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)
break;
auto [blocked, result] = lookupTableProp(*existingPropType, segment);
ValueContext ctx = i == c.path.size() - 1 ? ValueContext::LValue : ValueContext::RValue;
auto [blocked, result] = lookupTableProp(*existingPropType, segment, ctx);
if (!blocked.empty())
{
for (TypeId blocked : blocked)
@ -1356,8 +1380,8 @@ bool ConstraintSolver::tryDispatch(const SetPropConstraint& c, NotNull<const Con
if (existingPropType)
{
if (!isBlocked(c.propType))
unify(constraint->scope, constraint->location, c.propType, *existingPropType);
unify(constraint->scope, constraint->location, propType, *existingPropType);
unify(constraint->scope, constraint->location, *existingPropType, propType);
bind(c.resultType, c.subjectType);
unblock(c.resultType, constraint->location);
return true;
@ -1382,7 +1406,7 @@ bool ConstraintSolver::tryDispatch(const SetPropConstraint& c, NotNull<const Con
{
LUAU_ASSERT(!subjectType->persistent);
ttv->props[c.path[0]] = Property{c.propType};
ttv->props[c.path[0]] = Property{propType};
bind(c.resultType, subjectType);
unblock(c.resultType, constraint->location);
return true;
@ -1391,7 +1415,7 @@ bool ConstraintSolver::tryDispatch(const SetPropConstraint& c, NotNull<const Con
{
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)
{
// 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.resultType)->ty.emplace<BoundType>(subjectType);
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)
{
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);
mtt->indexer = TableIndexer{promotedIndexTy, c.propType};
@ -1486,6 +1510,7 @@ bool ConstraintSolver::tryDispatch(const UnpackConstraint& c, NotNull<const Cons
if (isBlocked(resultPack))
{
LUAU_ASSERT(resultPack != sourcePack);
asMutable(resultPack)->ty.emplace<BoundTypePack>(sourcePack);
unblock(resultPack, constraint->location);
return true;
@ -1525,7 +1550,7 @@ bool ConstraintSolver::tryDispatch(const UnpackConstraint& c, NotNull<const Cons
else
{
LUAU_ASSERT(c.resultIsLValue);
unify(constraint->scope, constraint->location, resultTy, srcTy);
unify(constraint, resultTy, srcTy);
}
unblock(resultTy, constraint->location);
@ -1553,7 +1578,7 @@ bool ConstraintSolver::tryDispatch(const UnpackConstraint& c, NotNull<const Cons
apply(resultTy, srcTy);
}
else
unify(constraint->scope, constraint->location, resultTy, srcTy);
unify(constraint, resultTy, srcTy);
++resultIter;
++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)
{
TypeId ty = follow(c.ty);
FamilyGraphReductionResult result = reduceFamilies(ty, constraint->location, TypeFamilyContext{NotNull{this}, constraint->scope}, force);
FamilyGraphReductionResult result =
reduceFamilies(ty, constraint->location, TypeFamilyContext{NotNull{this}, constraint->scope, constraint}, force);
for (TypeId r : result.reducedTypes)
unblock(r, constraint->location);
@ -1639,7 +1666,8 @@ bool ConstraintSolver::tryDispatch(const ReduceConstraint& c, NotNull<const Cons
bool ConstraintSolver::tryDispatch(const ReducePackConstraint& c, NotNull<const Constraint> constraint, bool force)
{
TypePackId tp = follow(c.tp);
FamilyGraphReductionResult result = reduceFamilies(tp, constraint->location, TypeFamilyContext{NotNull{this}, constraint->scope}, force);
FamilyGraphReductionResult result =
reduceFamilies(tp, constraint->location, TypeFamilyContext{NotNull{this}, constraint->scope, constraint}, force);
for (TypeId r : result.reducedTypes)
unblock(r, constraint->location);
@ -1659,6 +1687,13 @@ bool ConstraintSolver::tryDispatch(const ReducePackConstraint& c, NotNull<const
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)
{
auto block_ = [&](auto&& t) {
@ -1721,7 +1756,7 @@ bool ConstraintSolver::tryDispatchIterableTable(TypeId iteratorTy, const Iterabl
if (iteratorTable->indexer)
{
TypePackId expectedVariablePack = arena->addTypePack({iteratorTable->indexer->indexType, iteratorTable->indexer->indexResultType});
unify(constraint->scope, constraint->location, c.variables, expectedVariablePack);
unify(constraint, c.variables, expectedVariablePack);
auto [variableTys, variablesTail] = flatten(c.variables);
@ -1755,7 +1790,7 @@ bool ConstraintSolver::tryDispatchIterableTable(TypeId iteratorTy, const Iterabl
if (auto iterFtv = get<FunctionType>(*instantiatedIterFn))
{
TypePackId expectedIterArgs = arena->addTypePack({iteratorTy});
unify(constraint->scope, constraint->location, iterFtv->argTypes, expectedIterArgs);
unify(constraint, iterFtv->argTypes, expectedIterArgs);
TypePack iterRets = extendTypePack(*arena, builtinTypes, iterFtv->retTypes, 2);
@ -1779,7 +1814,7 @@ bool ConstraintSolver::tryDispatchIterableTable(TypeId iteratorTy, const Iterabl
const TypePackId nextRetPack = arena->addTypePack(TypePack{{firstIndex}, valueTailTy});
const TypeId expectedNextTy = arena->addType(FunctionType{nextArgPack, nextRetPack});
unify(constraint->scope, constraint->location, *instantiatedNextFn, expectedNextTy);
unify(constraint, *instantiatedNextFn, expectedNextTy);
pushConstraint(constraint->scope, constraint->location, UnpackConstraint{c.variables, nextRetPack});
}
@ -1849,13 +1884,11 @@ bool ConstraintSolver::tryDispatchIterableFunction(
const TypePackId nextRetPack = arena->addTypePack(TypePack{{retIndex}, valueTailTy});
const TypeId expectedNextTy = arena->addType(FunctionType{TypeLevel{}, constraint->scope, nextArgPack, nextRetPack});
std::optional<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 (!error)
if (ok)
(*c.astForInNextTypes)[c.nextAstFragment] = expectedNextTy;
else
reportError(*error);
auto it = begin(nextRetPack);
std::vector<TypeId> modifiedNextRetHead;
@ -1882,14 +1915,14 @@ bool ConstraintSolver::tryDispatchIterableFunction(
}
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};
return lookupTableProp(subjectType, propName, suppressSimplification, seen);
return lookupTableProp(subjectType, propName, context, suppressSimplification, seen);
}
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))
return {};
@ -1906,19 +1939,58 @@ std::pair<std::vector<TypeId>, std::optional<TypeId>> ConstraintSolver::lookupTa
else if (auto ttv = getMutable<TableType>(subjectType))
{
if (auto prop = ttv->props.find(propName); prop != ttv->props.end())
return {{}, FFlag::DebugLuauReadWriteProperties ? prop->second.readType() : prop->second.type()};
else if (ttv->indexer && maybeString(ttv->indexer->indexType))
{
switch (context)
{
case ValueContext::RValue:
if (auto rt = prop->second.readTy)
return {{}, rt};
break;
case ValueContext::LValue:
if (auto wt = prop->second.writeTy)
return {{}, wt};
break;
}
}
if (ttv->indexer && maybeString(ttv->indexer->indexType))
return {{}, ttv->indexer->indexResultType};
else if (ttv->state == TableState::Free)
if (ttv->state == TableState::Free)
{
TypeId result = freshType(arena, builtinTypes, ttv->scope);
ttv->props[propName] = Property{result};
switch (context)
{
case ValueContext::RValue:
ttv->props[propName].readTy = result;
break;
case ValueContext::LValue:
if (auto it = ttv->props.find(propName); it != ttv->props.end() && it->second.isReadOnly())
{
// We do infer read-only properties, but we do not infer
// separate read and write types.
//
// If we encounter a case where a free table has a read-only
// property that we subsequently sense a write to, we make
// the judgement that the property is read-write and that
// both the read and write types are the same.
Property& prop = it->second;
prop.writeTy = prop.readTy;
return {{}, *prop.readTy};
}
else
ttv->props[propName] = Property::rw(result);
break;
}
return {{}, result};
}
}
else if (auto mt = get<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)
return {blocked, result};
@ -1949,13 +2021,13 @@ std::pair<std::vector<TypeId>, std::optional<TypeId>> ConstraintSolver::lookupTa
}
}
else
return lookupTableProp(indexType, propName, suppressSimplification, seen);
return lookupTableProp(indexType, propName, context, suppressSimplification, seen);
}
}
else if (auto ct = get<ClassType>(subjectType))
{
if (auto p = lookupClassProp(ct, propName))
return {{}, p->type()};
return {{}, context == ValueContext::RValue ? p->readTy : p->writeTy};
if (ct->indexer)
{
return {{}, ct->indexer->indexResultType};
@ -1970,14 +2042,14 @@ std::pair<std::vector<TypeId>, std::optional<TypeId>> ConstraintSolver::lookupTa
if (indexProp == metatable->props.end())
return {{}, std::nullopt};
return lookupTableProp(indexProp->second.type(), propName, suppressSimplification, seen);
return lookupTableProp(indexProp->second.type(), propName, context, suppressSimplification, seen);
}
else if (auto ft = get<FreeType>(subjectType))
{
const TypeId upperBound = follow(ft->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.
@ -1987,7 +2059,16 @@ std::pair<std::vector<TypeId>, std::optional<TypeId>> ConstraintSolver::lookupTa
TableType* tt = getMutable<TableType>(newUpperBound);
LUAU_ASSERT(tt);
TypeId propType = freshType(arena, builtinTypes, scope);
tt->props[propName] = Property{propType};
switch (context)
{
case ValueContext::RValue:
tt->props[propName] = Property::readonly(propType);
break;
case ValueContext::LValue:
tt->props[propName] = Property::rw(propType);
break;
}
unify(scope, Location{}, subjectType, newUpperBound);
@ -2000,7 +2081,7 @@ std::pair<std::vector<TypeId>, std::optional<TypeId>> ConstraintSolver::lookupTa
for (TypeId ty : utv)
{
auto [innerBlocked, innerResult] = lookupTableProp(ty, propName, suppressSimplification, seen);
auto [innerBlocked, innerResult] = lookupTableProp(ty, propName, context, suppressSimplification, seen);
blocked.insert(blocked.end(), innerBlocked.begin(), innerBlocked.end());
if (innerResult)
options.insert(*innerResult);
@ -2029,7 +2110,7 @@ std::pair<std::vector<TypeId>, std::optional<TypeId>> ConstraintSolver::lookupTa
for (TypeId ty : itv)
{
auto [innerBlocked, innerResult] = lookupTableProp(ty, propName, suppressSimplification, seen);
auto [innerBlocked, innerResult] = lookupTableProp(ty, propName, context, suppressSimplification, seen);
blocked.insert(blocked.end(), innerBlocked.begin(), innerBlocked.end());
if (innerResult)
options.insert(*innerResult);
@ -2056,33 +2137,38 @@ std::pair<std::vector<TypeId>, std::optional<TypeId>> ConstraintSolver::lookupTa
}
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 (TypeId addition : additions)
upperBoundContributors[expanded].push_back(std::make_pair(constraint->location, addition));
upperBoundContributors[expanded].push_back(std::make_pair(location, addition));
}
}
else
{
// Unification only fails when doing so would fail the occurs check.
// ie create a self-bound type or a cyclic type pack
reportError(OccursCheckFailed{}, constraint->location);
reportError(OccursCheckFailed{}, location);
return false;
}
unblock(subTy, constraint->location);
unblock(superTy, constraint->location);
unblock(subType, location);
unblock(superType, location);
return true;
}
template<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)
{
resultTy = follow(resultTy);
@ -2297,6 +2383,21 @@ void ConstraintSolver::unblock(const std::vector<TypePackId>& packs, Location lo
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)
{
ty = follow(ty);
@ -2318,39 +2419,6 @@ bool ConstraintSolver::isBlocked(NotNull<const Constraint> constraint)
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)
{
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";
}
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
{
return "Checked Function " + e.functionName + " expects " + std::to_string(e.expected) + " arguments, but received " +
std::to_string(e.actual);
}
std::string operator()(const UnexpectedTypeInSubtyping& e) const
{
return "Encountered an unexpected type in subtyping: " + toString(e.ty);
}
std::string operator()(const UnexpectedTypePackInSubtyping& e) const
{
return "Encountered an unexpected type pack in subtyping: " + toString(e.tp);
}
};
struct InvalidNameChecker
@ -638,6 +663,11 @@ bool UnknownProperty::operator==(const UnknownProperty& rhs) const
return *table == *rhs.table && key == rhs.key;
}
bool PropertyAccessViolation::operator==(const PropertyAccessViolation& rhs) const
{
return *table == *rhs.table && key == rhs.key && context == rhs.context;
}
bool NotATable::operator==(const NotATable& rhs) const
{
return ty == rhs.ty;
@ -884,6 +914,16 @@ bool CheckedFunctionIncorrectArgs::operator==(const CheckedFunctionIncorrectArgs
return functionName == rhs.functionName && expected == rhs.expected && actual == rhs.actual;
}
bool UnexpectedTypeInSubtyping::operator==(const UnexpectedTypeInSubtyping& rhs) const
{
return ty == rhs.ty;
}
bool UnexpectedTypePackInSubtyping::operator==(const UnexpectedTypePackInSubtyping& rhs) const
{
return tp == rhs.tp;
}
std::string toString(const TypeError& error)
{
return toString(error, TypeErrorToStringOptions{});
@ -1059,9 +1099,15 @@ void copyError(T& e, TypeArena& destArena, CloneState& cloneState)
{
e.argumentType = clone(e.argumentType);
}
else if constexpr (std::is_same_v<T, PropertyAccessViolation>)
e.table = clone(e.table);
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
static_assert(always_false_v<T>, "Non-exhaustive type switch");
}

View file

@ -32,9 +32,8 @@ LUAU_FASTINT(LuauTypeInferRecursionLimit)
LUAU_FASTINT(LuauTarjanChildLimit)
LUAU_FASTFLAG(LuauInferInNoCheckMode)
LUAU_FASTFLAGVARIABLE(LuauKnowsTheDataModel3, false)
LUAU_FASTFLAGVARIABLE(DebugLuauDeferredConstraintResolution, false)
LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution)
LUAU_FASTFLAGVARIABLE(DebugLuauLogSolverToJson, false)
LUAU_FASTFLAGVARIABLE(DebugLuauReadWriteProperties, false)
LUAU_FASTFLAGVARIABLE(DebugLuauLogSolverToJsonFile, false)
namespace Luau
@ -1219,6 +1218,15 @@ ModulePtr check(const SourceModule& sourceModule, Mode mode, const std::vector<R
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)
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->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;
}

View file

@ -2,6 +2,7 @@
#include "Luau/Instantiation.h"
#include "Luau/Common.h"
#include "Luau/Instantiation2.h" // including for `Replacer` which was stolen since it will be kept in the new solver
#include "Luau/ToString.h"
#include "Luau/TxnLog.h"
#include "Luau/TypeArena.h"
@ -143,39 +144,6 @@ TypePackId ReplaceGenerics::clean(TypePackId tp)
return addTypePack(TypePackVar(FreeTypePack{scope, level}));
}
struct Replacer : Substitution
{
DenseHashMap<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(
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>)
stream << "NonStrictFunctionDefinitionError { functionName = '" + err.functionName + "', argument = '" + err.argument +
"', 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>)
stream << "CheckedFunction { functionName = '" + err.functionName + ", expected = " + std::to_string(err.expected) +
", 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
static_assert(always_false_v<T>, "Non-exhaustive type switch");
}

View file

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

View file

@ -9,6 +9,7 @@
#include "Luau/Common.h"
#include "Luau/RecursionCounter.h"
#include "Luau/Set.h"
#include "Luau/Simplify.h"
#include "Luau/Subtyping.h"
#include "Luau/Type.h"
#include "Luau/TypeFwd.h"
@ -20,7 +21,6 @@ LUAU_FASTFLAGVARIABLE(DebugLuauCheckNormalizeInvariant, false)
LUAU_FASTINTVARIABLE(LuauNormalizeIterationLimit, 1200);
LUAU_FASTINTVARIABLE(LuauNormalizeCacheLimit, 100000);
LUAU_FASTFLAG(LuauTransitiveSubtyping)
LUAU_FASTFLAG(DebugLuauReadWriteProperties)
LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution)
namespace Luau
@ -472,11 +472,11 @@ bool Normalizer::isInhabited(TypeId ty, Set<TypeId>& seen)
{
for (const auto& [_, prop] : ttv->props)
{
if (FFlag::DebugLuauReadWriteProperties)
if (FFlag::DebugLuauDeferredConstraintResolution)
{
// A table enclosing a read property whose type is uninhabitable is also itself uninhabitable,
// but not its write property. That just means the write property doesn't exist, and so is readonly.
if (auto ty = prop.readType(); ty && !isInhabited(*ty, seen))
if (auto ty = prop.readTy; ty && !isInhabited(*ty, seen))
return false;
}
else
@ -2349,11 +2349,60 @@ std::optional<TypeId> Normalizer::intersectionOfTables(TypeId here, TypeId there
{
const auto& [_name, tprop] = *tfound;
// TODO: variance issues here, which can't be fixed until we have read/write property types
if (FFlag::DebugLuauDeferredConstraintResolution)
{
if (hprop.readTy.has_value())
{
if (tprop.readTy.has_value())
{
TypeId ty = simplifyIntersection(builtinTypes, NotNull{arena}, *hprop.readTy, *tprop.readTy).result;
prop.readTy = ty;
hereSubThere &= (ty == hprop.readTy);
thereSubHere &= (ty == tprop.readTy);
}
else
{
prop.readTy = *hprop.readTy;
thereSubHere = false;
}
}
else if (tprop.readTy.has_value())
{
prop.readTy = *tprop.readTy;
hereSubThere = false;
}
if (hprop.writeTy.has_value())
{
if (tprop.writeTy.has_value())
{
prop.writeTy = simplifyIntersection(builtinTypes, NotNull{arena}, *hprop.writeTy, *tprop.writeTy).result;
hereSubThere &= (prop.writeTy == hprop.writeTy);
thereSubHere &= (prop.writeTy == tprop.writeTy);
}
else
{
prop.writeTy = *hprop.writeTy;
thereSubHere = false;
}
}
else if (tprop.writeTy.has_value())
{
prop.writeTy = *tprop.writeTy;
hereSubThere = false;
}
}
else
{
prop.setType(intersectionType(hprop.type(), tprop.type()));
hereSubThere &= (prop.type() == hprop.type());
thereSubHere &= (prop.type() == tprop.type());
}
}
// TODO: string indexers
if (prop.readTy || prop.writeTy)
result.props[name] = prop;
}
@ -2431,8 +2480,10 @@ void Normalizer::intersectTablesWithTable(TypeIds& heres, TypeId there)
{
TypeIds tmp;
for (TypeId here : heres)
{
if (std::optional<TypeId> inter = intersectionOfTables(here, there))
tmp.insert(*inter);
}
heres.retain(tmp);
heres.insert(tmp.begin(), tmp.end());
}
@ -2441,9 +2492,14 @@ void Normalizer::intersectTables(TypeIds& heres, const TypeIds& theres)
{
TypeIds tmp;
for (TypeId here : heres)
{
for (TypeId there : theres)
{
if (std::optional<TypeId> inter = intersectionOfTables(here, there))
tmp.insert(*inter);
}
}
heres.retain(tmp);
heres.insert(tmp.begin(), tmp.end());
}

View file

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

View file

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

View file

@ -7,7 +7,9 @@
#include "Luau/Normalize.h"
#include "Luau/Scope.h"
#include "Luau/StringUtils.h"
#include "Luau/Substitution.h"
#include "Luau/ToString.h"
#include "Luau/TxnLog.h"
#include "Luau/Type.h"
#include "Luau/TypeArena.h"
#include "Luau/TypeCheckLimits.h"
@ -217,7 +219,14 @@ SubtypingResult& SubtypingResult::withSuperPath(TypePath::Path path)
SubtypingResult& SubtypingResult::withErrors(ErrorVec& err)
{
errors = std::move(err);
for (TypeError& e : err)
errors.emplace_back(e);
return *this;
}
SubtypingResult& SubtypingResult::withError(TypeError err)
{
errors.push_back(std::move(err));
return *this;
}
@ -245,6 +254,74 @@ SubtypingResult SubtypingResult::any(const std::vector<SubtypingResult>& results
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,
NotNull<InternalErrorReporter> iceReporter, NotNull<Scope> scope)
: builtinTypes(builtinTypes)
@ -493,9 +570,19 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypeId sub
}
}
else if (auto subTypeFamilyInstance = get<TypeFamilyInstanceType>(subTy))
{
if (auto substSubTy = env.applyMappedGenerics(builtinTypes, arena, subTy))
subTypeFamilyInstance = get<TypeFamilyInstanceType>(*substSubTy);
result = isCovariantWith(env, subTypeFamilyInstance, 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);
}
else if (auto subGeneric = get<GenericType>(subTy); subGeneric && variance == Variance::Covariant)
{
bool ok = bindGeneric(env, subTy, superTy);
@ -604,7 +691,9 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypePackId
else if (get<ErrorTypePack>(*subTail))
return SubtypingResult{true}.withSubComponent(TypePath::PackField::Tail);
else
unexpected(*subTail);
return SubtypingResult{false}
.withSubComponent(TypePath::PackField::Tail)
.withError({scope->location, UnexpectedTypePackInSubtyping{*subTail}});
}
else
{
@ -656,7 +745,9 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypePackId
else if (get<ErrorTypePack>(*superTail))
return SubtypingResult{true}.withSuperComponent(TypePath::PackField::Tail);
else
unexpected(*superTail);
return SubtypingResult{false}
.withSuperComponent(TypePath::PackField::Tail)
.withError({scope->location, UnexpectedTypePackInSubtyping{*subTail}});
}
else
return {false};
@ -717,8 +808,10 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypePackId
// error type is fine on either side
results.push_back(SubtypingResult{true}.withBothComponent(TypePath::PackField::Tail));
else
iceReporter->ice(
format("Subtyping::isSubtype got unexpected type packs %s and %s", toString(*subTail).c_str(), toString(*superTail).c_str()));
return SubtypingResult{false}
.withBothComponent(TypePath::PackField::Tail)
.withError({scope->location, UnexpectedTypePackInSubtyping{*subTail}})
.withError({scope->location, UnexpectedTypePackInSubtyping{*superTail}});
}
else if (subTail)
{
@ -732,7 +825,9 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypePackId
return SubtypingResult{ok}.withSubComponent(TypePath::PackField::Tail);
}
else
unexpected(*subTail);
return SubtypingResult{false}
.withSubComponent(TypePath::PackField::Tail)
.withError({scope->location, UnexpectedTypePackInSubtyping{*subTail}});
}
else if (superTail)
{
@ -759,7 +854,9 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypePackId
results.push_back(SubtypingResult{false}.withSuperComponent(TypePath::PackField::Tail));
}
else
iceReporter->ice("Subtyping test encountered the unexpected type pack: " + toString(*superTail));
return SubtypingResult{false}
.withSuperComponent(TypePath::PackField::Tail)
.withError({scope->location, UnexpectedTypePackInSubtyping{*superTail}});
}
SubtypingResult result = SubtypingResult::all(results);
@ -1126,18 +1223,32 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const Tabl
if (subTable->props.empty() && !subTable->indexer && superTable->indexer)
return {false};
for (const auto& [name, prop] : superTable->props)
for (const auto& [name, superProp] : superTable->props)
{
std::vector<SubtypingResult> results;
if (auto it = subTable->props.find(name); it != subTable->props.end())
results.push_back(isInvariantWith(env, it->second.type(), prop.type()).withBothComponent(TypePath::Property(name)));
if (auto subIter = subTable->props.find(name); subIter != subTable->props.end())
results.push_back(isCovariantWith(env, subIter->second, superProp, name));
if (subTable->indexer)
{
if (isInvariantWith(env, subTable->indexer->indexType, builtinTypes->stringType).isSubtype)
results.push_back(isInvariantWith(env, subTable->indexer->indexResultType, prop.type())
if (isCovariantWith(env, builtinTypes->stringType, subTable->indexer->indexType).isSubtype)
{
if (superProp.isShared())
results.push_back(isInvariantWith(env, subTable->indexer->indexResultType, superProp.type())
.withSubComponent(TypePath::TypeField::IndexResult)
.withSuperComponent(TypePath::Property(name)));
.withSuperComponent(TypePath::Property::read(name)));
else
{
if (superProp.readTy)
results.push_back(isCovariantWith(env, subTable->indexer->indexResultType, *superProp.readTy)
.withSubComponent(TypePath::TypeField::IndexResult)
.withSuperComponent(TypePath::Property::read(name)));
if (superProp.writeTy)
results.push_back(isContravariantWith(env, subTable->indexer->indexResultType, *superProp.writeTy)
.withSubComponent(TypePath::TypeField::IndexResult)
.withSuperComponent(TypePath::Property::write(name)));
}
}
}
if (results.empty())
@ -1197,7 +1308,9 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const Clas
for (const auto& [name, prop] : superTable->props)
{
if (auto classProp = lookupClassProp(subClass, name))
result.andAlso(isInvariantWith(env, prop.type(), classProp->type()).withBothComponent(TypePath::Property(name)));
{
result.andAlso(isCovariantWith(env, *classProp, prop, name));
}
else
return SubtypingResult{false};
}
@ -1230,7 +1343,7 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const Prim
{
if (auto stringTable = get<TableType>(it->second.type()))
result.orElse(
isCovariantWith(env, stringTable, superTable).withSubPath(TypePath::PathBuilder().mt().prop("__index").build()));
isCovariantWith(env, stringTable, superTable).withSubPath(TypePath::PathBuilder().mt().readProp("__index").build()));
}
}
}
@ -1252,7 +1365,7 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const Sing
{
if (auto stringTable = get<TableType>(it->second.type()))
result.orElse(
isCovariantWith(env, stringTable, superTable).withSubPath(TypePath::PathBuilder().mt().prop("__index").build()));
isCovariantWith(env, stringTable, superTable).withSubPath(TypePath::PathBuilder().mt().readProp("__index").build()));
}
}
}
@ -1267,6 +1380,31 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const Tabl
.andAlso(isInvariantWith(env, subIndexer.indexResultType, superIndexer.indexResultType).withBothComponent(TypePath::TypeField::IndexResult));
}
SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const Property& subProp, const Property& superProp, const std::string& name)
{
SubtypingResult res{true};
if (superProp.isShared() && subProp.isShared())
res.andAlso(isInvariantWith(env, subProp.type(), superProp.type()).withBothComponent(TypePath::Property::read(name)));
else
{
if (superProp.readTy.has_value() && subProp.readTy.has_value())
res.andAlso(isCovariantWith(env, *subProp.readTy, *superProp.readTy).withBothComponent(TypePath::Property::read(name)));
if (superProp.writeTy.has_value() && subProp.writeTy.has_value())
res.andAlso(isContravariantWith(env, *subProp.writeTy, *superProp.writeTy).withBothComponent(TypePath::Property::write(name)));
if (superProp.isReadWrite())
{
if (subProp.isReadOnly())
res.andAlso(SubtypingResult{false}.withBothComponent(TypePath::Property::read(name)));
else if (subProp.isWriteOnly())
res.andAlso(SubtypingResult{false}.withBothComponent(TypePath::Property::write(name)));
}
}
return res;
}
SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const NormalizedType* subNorm, const NormalizedType* superNorm)
{
if (!subNorm || !superNorm)
@ -1473,14 +1611,4 @@ TypeId Subtyping::makeAggregateType(const Container& container, TypeId orElse)
return arena->addType(T{std::vector<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

View file

@ -374,7 +374,7 @@ struct TypeStringifier
tv->ty);
}
void stringify(const std::string& name, const Property& prop)
void emitKey(const std::string& name)
{
if (isIdentifier(name))
state.emit(name);
@ -385,30 +385,45 @@ struct TypeStringifier
state.emit("\"]");
}
state.emit(": ");
}
if (FFlag::DebugLuauReadWriteProperties)
void _newStringify(const std::string& name, const Property& prop)
{
// We special case the stringification if the property's read and write types are shared.
bool comma = false;
if (prop.isShared())
return stringify(*prop.readType());
{
emitKey(name);
stringify(prop.type());
return;
}
// Otherwise emit them separately.
if (auto ty = prop.readType())
if (prop.readTy)
{
state.emit("read ");
stringify(*ty);
emitKey(name);
stringify(*prop.readTy);
comma = true;
}
if (prop.readType() && prop.writeType())
state.emit(" + ");
if (auto ty = prop.writeType())
if (prop.writeTy)
{
if (comma)
{
state.emit(",");
state.newline();
}
state.emit("write ");
stringify(*ty);
emitKey(name);
stringify(*prop.writeTy);
}
}
else
void stringify(const std::string& name, const Property& prop)
{
if (FFlag::DebugLuauDeferredConstraintResolution)
return _newStringify(name, prop);
emitKey(name);
stringify(prop.type());
}
@ -1755,7 +1770,7 @@ std::string toString(const Constraint& constraint, ToStringOptions& opts)
}
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>)
{
@ -1801,6 +1816,8 @@ std::string toString(const Constraint& constraint, ToStringOptions& opts)
{
return "reduce " + tos(c.tp);
}
else if constexpr (std::is_same_v<T, EqualityConstraint>)
return "equality: " + tos(c.resultType) + " ~ " + tos(c.assignmentType);
else
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_FASTINT(LuauTypeInferRecursionLimit)
LUAU_FASTFLAG(LuauInstantiateInSubtyping)
LUAU_FASTFLAG(DebugLuauReadWriteProperties)
namespace Luau
{
@ -632,13 +631,10 @@ Property::Property(TypeId readTy, bool deprecated, const std::string& deprecated
, readTy(readTy)
, writeTy(readTy)
{
LUAU_ASSERT(!FFlag::DebugLuauReadWriteProperties);
}
Property Property::readonly(TypeId ty)
{
LUAU_ASSERT(FFlag::DebugLuauReadWriteProperties);
Property p;
p.readTy = ty;
return p;
@ -646,8 +642,6 @@ Property Property::readonly(TypeId ty)
Property Property::writeonly(TypeId ty)
{
LUAU_ASSERT(FFlag::DebugLuauReadWriteProperties);
Property p;
p.writeTy = ty;
return p;
@ -660,8 +654,6 @@ Property Property::rw(TypeId ty)
Property Property::rw(TypeId read, TypeId write)
{
LUAU_ASSERT(FFlag::DebugLuauReadWriteProperties);
Property p;
p.readTy = read;
p.writeTy = write;
@ -683,29 +675,15 @@ Property Property::create(std::optional<TypeId> read, std::optional<TypeId> writ
TypeId Property::type() const
{
LUAU_ASSERT(!FFlag::DebugLuauReadWriteProperties);
LUAU_ASSERT(readTy);
return *readTy;
}
void Property::setType(TypeId ty)
{
LUAU_ASSERT(!FFlag::DebugLuauReadWriteProperties);
readTy = ty;
}
std::optional<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;
if (FFlag::DebugLuauDeferredConstraintResolution)
writeTy = ty;
}
bool Property::isShared() const
@ -713,6 +691,21 @@ bool Property::isShared() const
return readTy && writeTy && readTy == writeTy;
}
bool Property::isReadOnly() const
{
return readTy && !writeTy;
}
bool Property::isWriteOnly() const
{
return !readTy && writeTy;
}
bool Property::isReadWrite() const
{
return readTy && writeTy;
}
TableType::TableType(TableState state, TypeLevel level, Scope* scope)
: state(state)
, level(level)
@ -961,6 +954,7 @@ BuiltinTypes::BuiltinTypes()
, optionalStringType(arena->addType(Type{UnionType{{stringType, nilType}}, /*persistent*/ true}))
, emptyTypePack(arena->addTypePack(TypePackVar{TypePack{{}}, /*persistent*/ true}))
, anyTypePack(arena->addTypePack(TypePackVar{VariadicTypePack{anyType}, /*persistent*/ true}))
, unknownTypePack(arena->addTypePack(TypePackVar{VariadicTypePack{unknownType}, /*persistent*/ true}))
, neverTypePack(arena->addTypePack(TypePackVar{VariadicTypePack{neverType}, /*persistent*/ true}))
, uninhabitableTypePack(arena->addTypePack(TypePackVar{TypePack{{neverType}, neverTypePack}, /*persistent*/ true}))
, errorTypePack(arena->addTypePack(TypePackVar{Unifiable::Error{}, /*persistent*/ true}))

View file

@ -1460,7 +1460,7 @@ struct TypeChecker2
{
visit(expr, ValueContext::RValue);
TypeId leftType = stripFromNilAndReport(lookupType(expr), location);
checkIndexTypeFromType(leftType, propName, location, context, astIndexExprTy);
checkIndexTypeFromType(leftType, propName, context, location, astIndexExprTy);
}
void visit(AstExprIndexName* indexName, ValueContext context)
@ -1709,8 +1709,8 @@ struct TypeChecker2
TypeId visit(AstExprBinary* expr, AstNode* overrideKey = nullptr)
{
visit(expr->left, ValueContext::LValue);
visit(expr->right, ValueContext::LValue);
visit(expr->left, ValueContext::RValue);
visit(expr->right, ValueContext::RValue);
NotNull<Scope> scope = stack.back();
@ -2534,20 +2534,16 @@ struct TypeChecker2
reportError(std::move(e));
}
// If the provided type does not have the named property, report an error.
void checkIndexTypeFromType(TypeId tableTy, const std::string& prop, const Location& location, ValueContext context, TypeId astIndexExprType)
/* A helper for checkIndexTypeFromType.
*
* Returns a pair:
* * A boolean indicating that at least one of the constituent types
* contains the prop, and
* * A vector of types that do not contain the prop.
*/
std::pair<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;
std::vector<TypeId> typesMissingTheProp;
@ -2556,7 +2552,7 @@ struct TypeChecker2
return;
DenseHashSet<TypeId> seen{nullptr};
bool found = hasIndexTypeFromType(ty, prop, location, seen, astIndexExprType);
bool found = hasIndexTypeFromType(ty, prop, context, location, seen, astIndexExprType, errors);
foundOneProp |= found;
if (!found)
typesMissingTheProp.push_back(ty);
@ -2601,6 +2597,26 @@ struct TypeChecker2
fetch(tyvar);
}
return {foundOneProp, typesMissingTheProp};
}
// If the provided type does not have the named property, report an error.
void checkIndexTypeFromType(TypeId tableTy, const std::string& prop, ValueContext context, const Location& location, TypeId astIndexExprType)
{
const NormalizedType* norm = normalizer.normalize(tableTy);
if (!norm)
{
reportError(NormalizationTooComplex{}, location);
return;
}
// if the type is error suppressing, we don't actually have any work left to do.
if (norm->shouldSuppressErrors())
return;
std::vector<TypeError> dummy;
const auto [foundOneProp, typesMissingTheProp] = lookupProp(norm, prop, context, location, astIndexExprType, module->errors);
if (!typesMissingTheProp.empty())
{
if (foundOneProp)
@ -2611,17 +2627,29 @@ struct TypeChecker2
// the `else` branch.
else if (context == ValueContext::LValue && !get<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);
else
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
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
// other codepath will do the right thing and signal false if the
@ -2635,14 +2663,14 @@ struct TypeChecker2
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);
ty = *mtIndex;
}
if (auto tt = getTableType(ty))
{
if (findTablePropertyRespectingMeta(builtinTypes, module->errors, ty, prop, location))
if (findTablePropertyRespectingMeta(builtinTypes, errors, ty, prop, context, location))
return true;
if (tt->indexer)
@ -2674,11 +2702,11 @@ struct TypeChecker2
}
else if (const UnionType* utv = get<UnionType>(ty))
return std::all_of(begin(utv), end(utv), [&](TypeId part) {
return hasIndexTypeFromType(part, prop, location, seen, astIndexExprType);
return hasIndexTypeFromType(part, prop, context, location, seen, astIndexExprType, errors);
});
else if (const IntersectionType* itv = get<IntersectionType>(ty))
return std::any_of(begin(itv), end(itv), [&](TypeId part) {
return hasIndexTypeFromType(part, prop, location, seen, astIndexExprType);
return hasIndexTypeFromType(part, prop, context, location, seen, astIndexExprType, errors);
});
else
return false;

View file

@ -7,6 +7,7 @@
#include "Luau/DenseHash.h"
#include "Luau/Instantiation.h"
#include "Luau/Normalize.h"
#include "Luau/NotNull.h"
#include "Luau/Simplify.h"
#include "Luau/Substitution.h"
#include "Luau/Subtyping.h"
@ -853,6 +854,27 @@ static TypeFamilyReductionResult<TypeId> comparisonFamilyFn(TypeId instance, con
TypeId lhsTy = follow(typeParams.at(0));
TypeId rhsTy = follow(typeParams.at(1));
// Algebra Reduction Rules for comparison family functions
// Note that comparing to never tells you nothing about the other operand
// lt< 'a , never> -> continue
// lt< never, 'a> -> continue
// lt< 'a, t> -> 'a is t - we'll solve the constraint, return and solve lt<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
if (isPending(lhsTy, ctx->solver))
return {std::nullopt, false, {lhsTy}, {}};
@ -1248,8 +1270,8 @@ TypeFamilyReductionResult<TypeId> keyofFamilyImpl(
if (!normTy)
return {std::nullopt, false, {}, {}};
// if we don't have either just tables or just classes, we've got nothing to get keys of (at least until a future version perhaps adds classes as
// well)
// if we don't have either just tables or just classes, we've got nothing to get keys of (at least until a future version perhaps adds classes
// as well)
if (normTy->hasTables() == normTy->hasClasses())
return {std::nullopt, true, {}, {}};

View file

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

View file

@ -44,6 +44,12 @@ std::optional<TypeId> findMetatableEntry(
std::optional<TypeId> findTablePropertyRespectingMeta(
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))
return ty;
@ -52,8 +58,21 @@ std::optional<TypeId> findTablePropertyRespectingMeta(
{
const auto& it = tableType->props.find(name);
if (it != tableType->props.end())
{
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);
int count = 0;

View file

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

View file

@ -18,6 +18,7 @@ LUAU_FASTINTVARIABLE(LuauParseErrorLimit, 100)
// See docs/SyntaxChanges.md for an explanation.
LUAU_FASTFLAG(LuauCheckedFunctionSyntax)
LUAU_FASTFLAGVARIABLE(LuauReadWritePropertySyntax, false)
LUAU_FASTFLAGVARIABLE(DebugLuauDeferredConstraintResolution, false)
namespace Luau
{
@ -1339,7 +1340,7 @@ AstType* Parser::parseTableType(bool inDeclarationContext)
AstTableAccess access = AstTableAccess::ReadWrite;
std::optional<Location> accessLocation;
if (FFlag::LuauReadWritePropertySyntax)
if (FFlag::LuauReadWritePropertySyntax || FFlag::DebugLuauDeferredConstraintResolution)
{
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)
{
// Early out once we reach --program-args,-a since the remaining args are passed to lua
if (strcmp(argv[i], "--program-args") == 0 || strcmp(argv[i], "-a") == 0)
return files;
// Treat '-' as a special file whose source is read from stdin
// All other arguments that start with '-' are skipped
if (argv[i][0] == '-' && argv[i][1] != '\0')

View file

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

View file

@ -46,6 +46,8 @@ LUAU_FASTFLAGVARIABLE(LuauUpdatedRequireByStringSemantics, false)
constexpr int MaxTraversalLimit = 50;
static bool codegen = false;
static int program_argc = 0;
char** program_argv = nullptr;
// Ctrl-C handling
static void sigintCallback(lua_State* L, int gc)
@ -318,6 +320,12 @@ void setupState(lua_State* L)
luaL_sandbox(L);
}
void setupArguments(lua_State* L, int argc, char** argv)
{
for (int i = 0; i < argc; ++i)
lua_pushstring(L, argv[i]);
}
std::string runCode(lua_State* L, const std::string& source)
{
std::string bytecode = Luau::compile(source, copts());
@ -668,7 +676,8 @@ static bool runFile(const char* name, lua_State* GL, bool repl)
if (coverageActive())
coverageTrack(L, -1);
status = lua_resume(L, NULL, 0);
setupArguments(L, program_argc, program_argv);
status = lua_resume(L, NULL, program_argc);
}
else
{
@ -704,7 +713,7 @@ static bool runFile(const char* name, lua_State* GL, bool repl)
static void displayHelp(const char* argv0)
{
printf("Usage: %s [options] [file list]\n", argv0);
printf("Usage: %s [options] [file list] [-a] [arg list]\n", argv0);
printf("\n");
printf("When file list is omitted, an interactive REPL is started instead.\n");
printf("\n");
@ -717,6 +726,7 @@ static void displayHelp(const char* argv0)
printf(" --profile[=N]: profile the code using N Hz sampling (default 10000) and output results to profile.out\n");
printf(" --timetrace: record compiler time tracing information into trace.json\n");
printf(" --codegen: execute code using native code generation\n");
printf(" --program-args,-a: declare start of arguments to be passed to the Luau program");
}
static int assertionHandler(const char* expr, const char* file, int line, const char* function)
@ -739,6 +749,7 @@ int replMain(int argc, char** argv)
bool coverage = false;
bool interactive = false;
bool codegenPerf = false;
int program_args = argc;
for (int i = 1; i < argc; i++)
{
@ -800,6 +811,11 @@ int replMain(int argc, char** argv)
{
setLuauFlags(argv[i] + 9);
}
else if (strcmp(argv[i], "--program-args") == 0 || strcmp(argv[i], "-a") == 0)
{
program_args = i + 1;
break;
}
else if (argv[i][0] == '-')
{
fprintf(stderr, "Error: Unrecognized option '%s'.\n\n", argv[i]);
@ -808,6 +824,10 @@ int replMain(int argc, char** argv)
}
}
program_argc = argc - program_args;
program_argv = &argv[program_args];
#if !defined(LUAU_ENABLE_TIME_TRACE)
if (FFlag::DebugLuauTimeTracing)
{

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
LUAU_FASTINTVARIABLE(CodegenHeuristicsBlockInstructionLimit, 65'536) // 64 K
LUAU_FASTFLAGVARIABLE(DisableNativeCodegenIfBreakpointIsSet, false)
namespace Luau
{
namespace CodeGen
@ -302,7 +300,7 @@ void create(lua_State* L, AllocationCallback* allocationCallback, void* allocati
ecb->close = onCloseState;
ecb->destroy = onDestroyFunction;
ecb->enter = onEnter;
ecb->disable = FFlag::DisableNativeCodegenIfBreakpointIsSet ? onDisable : nullptr;
ecb->disable = onDisable;
}
void create(lua_State* L)

View file

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

View file

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

View file

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

View file

@ -17,7 +17,7 @@ static TypeId requireBinding(Scope* scope, const char* name)
TEST_SUITE_BEGIN("ConstraintSolver");
TEST_CASE_FIXTURE(ConstraintGeneratorFixture, "hello")
TEST_CASE_FIXTURE(ConstraintGeneratorFixture, "constraint_basics")
{
solve(R"(
local a = 55
@ -58,12 +58,7 @@ TEST_CASE_FIXTURE(ConstraintGeneratorFixture, "proper_let_generalization")
TypeId idType = requireBinding(rootScope, "b");
ToStringOptions opts;
if (FFlag::DebugLuauDeferredConstraintResolution)
CHECK("(unknown) -> number" == toString(idType, opts));
else
CHECK("<a>(a) -> number" == toString(idType, opts));
CHECK("(unknown) -> number" == toString(idType));
}
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")
{
ScopedFastFlag sff{FFlag::DebugLuauDeferredConstraintResolution, false};
CheckResult result = check(R"(
local function id<a>(x: a): a
return x

View file

@ -7,6 +7,8 @@
#include "doctest.h"
LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution);
using namespace Luau;
TEST_SUITE_BEGIN("Linter");
@ -1246,6 +1248,30 @@ _ = {
CHECK_EQ(result.warnings[5].text, "Table index 1 is a duplicate; previously defined at line 36");
}
TEST_CASE_FIXTURE(Fixture, "read_write_table_props")
{
ScopedFastFlag sff{FFlag::DebugLuauDeferredConstraintResolution, true};
LintResult result = lint(R"(-- line 1
type A = {x: number}
type B = {read x: number, write x: number}
type C = {x: number, read x: number} -- line 4
type D = {x: number, write x: number}
type E = {read x: number, x: boolean}
type F = {read x: number, read x: number}
type G = {write x: number, x: boolean}
type H = {write x: number, write x: boolean}
)");
REQUIRE(6 == result.warnings.size());
CHECK(result.warnings[0].text == "Table type field 'x' is already read-write; previously defined at line 4");
CHECK(result.warnings[1].text == "Table type field 'x' is already read-write; previously defined at line 5");
CHECK(result.warnings[2].text == "Table type field 'x' already has a read type defined at line 6");
CHECK(result.warnings[3].text == "Table type field 'x' is a duplicate; previously defined at line 7");
CHECK(result.warnings[4].text == "Table type field 'x' already has a write type defined at line 8");
CHECK(result.warnings[5].text == "Table type field 'x' is a duplicate; previously defined at line 9");
}
TEST_CASE_FIXTURE(Fixture, "ImportOnlyUsedInTypeAnnotation")
{
LintResult result = lint(R"(

View file

@ -940,4 +940,26 @@ TEST_CASE_FIXTURE(NormalizeFixture, "normalize_unknown")
CHECK(toString(normalizer.typeFromNormal(*nt)) == "unknown");
}
TEST_CASE_FIXTURE(NormalizeFixture, "read_only_props")
{
ScopedFastFlag sff{FFlag::DebugLuauDeferredConstraintResolution, true};
CHECK("{ x: string }" == toString(normal("{ read x: string } & { x: string }"), {true}));
CHECK("{ x: string }" == toString(normal("{ x: string } & { read x: string }"), {true}));
}
TEST_CASE_FIXTURE(NormalizeFixture, "read_only_props_2")
{
ScopedFastFlag sff{FFlag::DebugLuauDeferredConstraintResolution, true};
CHECK(R"({ x: never })" == toString(normal(R"({ x: "hello" } & { x: "world" })"), {true}));
}
TEST_CASE_FIXTURE(NormalizeFixture, "read_only_props_3")
{
ScopedFastFlag sff{FFlag::DebugLuauDeferredConstraintResolution, true};
CHECK("{ read x: never }" == toString(normal(R"({ read x: "hello" } & { read x: "world" })"), {true}));
}
TEST_SUITE_END();

View file

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

View file

@ -935,14 +935,13 @@ TEST_CASE_FIXTURE(Fixture, "tostring_error_mismatch")
function f1() : {a : number, b : string, c : { d : number}}
return { a = 1, b = "a", c = {d = "a"}}
end
)");
//clang-format off
std::string expected =
(FFlag::DebugLuauDeferredConstraintResolution)
? R"(Type pack '{| a: number, b: string, c: {| d: string |} |}' could not be converted into '{ a: number, b: string, c: { d: number } }'; at [0]["c"]["d"], string is not exactly number)"
:
R"(Type
std::string expected;
if (FFlag::DebugLuauDeferredConstraintResolution)
expected = R"(Type pack '{ a: number, b: string, c: { d: string } }' could not be converted into '{ a: number, b: string, c: { d: number } }'; at [0][read "c"][read "d"], string is not exactly number)";
else
expected = R"(Type
'{ a: number, b: string, c: { d: string } }'
could not be converted into
'{| a: number, b: string, c: {| d: number |} |}'
@ -955,7 +954,6 @@ could not be converted into
caused by:
Property 'd' is not compatible.
Type 'string' could not be converted into 'number' in an invariant context)";
//clang-format on
LUAU_REQUIRE_ERROR_COUNT(1, result);
@ -984,4 +982,20 @@ local f = abs
TypeId fn = requireType("f");
CHECK("@checked (number) -> number" == toString(fn));
}
TEST_CASE_FIXTURE(Fixture, "read_only_properties")
{
ScopedFastFlag sff{FFlag::DebugLuauDeferredConstraintResolution, true};
CheckResult result = check(R"(
type A = {x: string}
type B = {read x: string}
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK("{ x: string }" == toString(requireTypeAlias("A"), {true}));
CHECK("{ read x: string }" == toString(requireTypeAlias("B"), {true}));
}
TEST_SUITE_END();

View file

@ -202,7 +202,7 @@ TEST_CASE_FIXTURE(Fixture, "generic_aliases")
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
const std::string expected = R"(Type 'bad' could not be converted into 'T<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_EQ(expected, toString(result.errors[0]));
}
@ -221,7 +221,7 @@ TEST_CASE_FIXTURE(Fixture, "dependent_generic_aliases")
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
const std::string expected = R"(Type 'bad' could not be converted into 'U<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_EQ(expected, toString(result.errors[0]));

View file

@ -386,9 +386,6 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "setmetatable_on_union_of_tables")
LUAU_REQUIRE_NO_ERRORS(result);
if (FFlag::DebugLuauDeferredConstraintResolution)
CHECK("{ @metatable {| |}, A } | { @metatable {| |}, B }" == toString(requireTypeAlias("X")));
else
CHECK("{ @metatable { }, A } | { @metatable { }, B }" == toString(requireTypeAlias("X")));
}
@ -1012,6 +1009,22 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "assert_removes_falsy_types2")
CHECK_EQ("((boolean | number)?) -> number | true", toString(requireType("f")));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "assert_removes_falsy_types3")
{
CheckResult result = check(R"(
local function f(x: (number | boolean)?)
assert(x)
return x
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
if (FFlag::DebugLuauDeferredConstraintResolution)
CHECK_EQ("((boolean | number)?) -> number | true", toString(requireType("f")));
else // without the annotation, the old solver doesn't infer the best return type here
CHECK_EQ("((boolean | number)?) -> boolean | number", toString(requireType("f")));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "assert_removes_falsy_types_even_from_type_pack_tail_but_only_for_the_first_type")
{
CheckResult result = check(R"(

View file

@ -463,7 +463,7 @@ local b: B = a
LUAU_REQUIRE_ERRORS(result);
if (FFlag::DebugLuauDeferredConstraintResolution)
CHECK(toString(result.errors.at(0)) == "Type 'a' could not be converted into 'B'; at [\"x\"], ChildClass is not exactly BaseClass");
CHECK(toString(result.errors.at(0)) == "Type 'a' could not be converted into 'B'; at [read \"x\"], ChildClass is not exactly BaseClass");
else
{
const std::string expected = R"(Type 'A' could not be converted into 'B'
@ -639,4 +639,58 @@ TEST_CASE_FIXTURE(ClassFixture, "indexable_classes")
}
}
TEST_CASE_FIXTURE(Fixture, "read_write_class_properties")
{
ScopedFastFlag sff{FFlag::DebugLuauDeferredConstraintResolution, true};
TypeArena& arena = frontend.globals.globalTypes;
unfreeze(arena);
TypeId instanceType = arena.addType(ClassType{"Instance", {}, nullopt, nullopt, {}, {}, "Test"});
getMutable<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();

View file

@ -27,6 +27,8 @@ TEST_CASE_FIXTURE(Fixture, "check_generic_function")
local y: number = id(37)
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ(builtinTypes->stringType, requireType("x"));
CHECK_EQ(builtinTypes->numberType, requireType("y"));
}
TEST_CASE_FIXTURE(Fixture, "check_generic_local_function")
@ -39,6 +41,40 @@ TEST_CASE_FIXTURE(Fixture, "check_generic_local_function")
local y: number = id(37)
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ(builtinTypes->stringType, requireType("x"));
CHECK_EQ(builtinTypes->numberType, requireType("y"));
}
TEST_CASE_FIXTURE(Fixture, "check_generic_local_function2")
{
CheckResult result = check(R"(
local function id<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")
@ -370,7 +406,7 @@ TEST_CASE_FIXTURE(Fixture, "calling_self_generic_methods")
{
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}));
}
else
@ -437,6 +473,7 @@ TEST_CASE_FIXTURE(Fixture, "dont_leak_generic_types")
-- so this assignment should fail
local b: boolean = f(true)
)");
LUAU_REQUIRE_ERRORS(result);
}
@ -797,8 +834,9 @@ y.a.c = y
LUAU_REQUIRE_ERRORS(result);
if (FFlag::DebugLuauDeferredConstraintResolution)
CHECK(toString(result.errors.at(0)) ==
R"(Type 'x' could not be converted into 'T<number>'; type x["a"]["c"] (nil) is not exactly T<number>["a"]["c"][0] (T<number>))");
CHECK(
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
{
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")));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "generalization_no_cyclic_intersections")
{
CheckResult result = check(R"(
local f, t, n = pairs({"foo"})
local k, v = f(t)
)");
CHECK("({string}, number?) -> (number?, string)" == toString(requireType("f")));
CHECK("{string}" == toString(requireType("t")));
CHECK("number?" == toString(requireType("k")));
CHECK("string" == toString(requireType("v")));
}
TEST_CASE_FIXTURE(Fixture, "missing_generic_type_parameter")
{
CheckResult result = check(R"(
@ -1381,4 +1432,20 @@ TEST_CASE_FIXTURE(Fixture, "missing_generic_type_parameter")
REQUIRE(get<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();

View file

@ -414,7 +414,7 @@ local b: B.T = a
LUAU_REQUIRE_ERROR_COUNT(1, result);
if (FFlag::DebugLuauDeferredConstraintResolution)
CHECK(toString(result.errors.at(0)) == "Type 'a' could not be converted into 'T'; at [\"x\"], number is not exactly string");
CHECK(toString(result.errors.at(0)) == "Type 'a' could not be converted into 'T'; at [read \"x\"], number is not exactly string");
else
{
const std::string expected = R"(Type 'T' from 'game/A' could not be converted into 'T' from 'game/B'
@ -455,7 +455,7 @@ local b: B.T = a
LUAU_REQUIRE_ERROR_COUNT(1, result);
if (FFlag::DebugLuauDeferredConstraintResolution)
CHECK(toString(result.errors.at(0)) == "Type 'a' could not be converted into 'T'; at [\"x\"], number is not exactly string");
CHECK(toString(result.errors.at(0)) == "Type 'a' could not be converted into 'T'; at [read \"x\"], number is not exactly string");
else
{
const std::string expected = R"(Type 'T' from 'game/B' could not be converted into 'T' from 'game/C'

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>
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);

View file

@ -357,7 +357,7 @@ TEST_CASE_FIXTURE(Fixture, "open_table_unification_2")
LUAU_REQUIRE_ERROR_COUNT(1, result);
TypeError& err = result.errors[0];
MissingProperties* error = get<MissingProperties>(err);
REQUIRE(error != nullptr);
REQUIRE_MESSAGE(error != nullptr, "Expected MissingProperties but got " << toString(err));
REQUIRE(error->properties.size() == 1);
CHECK_EQ("y", error->properties[0]);
@ -426,7 +426,7 @@ TEST_CASE_FIXTURE(Fixture, "table_param_width_subtyping_2")
LUAU_REQUIRE_ERROR_COUNT(1, result);
MissingProperties* error = get<MissingProperties>(result.errors[0]);
REQUIRE(error != nullptr);
REQUIRE_MESSAGE(error != nullptr, "Expected MissingProperties but got " << toString(result.errors[0]));
REQUIRE(error->properties.size() == 1);
CHECK_EQ("baz", error->properties[0]);
@ -446,7 +446,7 @@ TEST_CASE_FIXTURE(Fixture, "table_param_width_subtyping_3")
LUAU_REQUIRE_ERROR_COUNT(1, result);
TypeError& err = result.errors[0];
MissingProperties* error = get<MissingProperties>(err);
REQUIRE(error != nullptr);
REQUIRE_MESSAGE(error != nullptr, "Expected MissingProperties but got " << toString(err));
REQUIRE(error->properties.size() == 1);
CHECK_EQ("baz", error->properties[0]);
@ -461,52 +461,6 @@ TEST_CASE_FIXTURE(Fixture, "table_param_width_subtyping_3")
CHECK_EQ(err.location, (Location{Position{6, 8}, Position{6, 9}}));
}
#if 0
TEST_CASE_FIXTURE(Fixture, "table_param_width_subtyping_2")
{
CheckResult result = check(R"(
function id(x)
return x
end
function foo(o)
id(o.x)
id(o.y)
return o
end
local a = {x=55, y=nil, w=3.14159}
local b = {}
b.x = 1
b.y = 'hello'
b.z = 'something extra!'
local q = foo(a) -- line 17
local w = foo(b) -- line 18
)");
LUAU_REQUIRE_NO_ERRORS(result);
TypeId qType = requireType("q");
const TableType* qTable = get<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")
{
CheckResult result = check(R"(
@ -680,7 +634,8 @@ TEST_CASE_FIXTURE(Fixture, "indexers_get_quantified_too")
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")
@ -1077,6 +1032,8 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "meta_add_inferred")
TEST_CASE_FIXTURE(BuiltinsFixture, "meta_add_both_ways")
{
ScopedFastFlag sff{FFlag::DebugLuauDeferredConstraintResolution, false};
CheckResult result = check(R"(
type VectorMt = { __add: (Vector, number) -> Vector }
local vectorMt: VectorMt
@ -1093,6 +1050,30 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "meta_add_both_ways")
CHECK_EQ(*requireType("a"), *requireType("c"));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "meta_add_both_ways_lti")
{
ScopedFastFlag sff{FFlag::DebugLuauDeferredConstraintResolution, true};
CheckResult result = check(R"(
local vectorMt = {}
function vectorMt.__add(self: Vector, other: number)
return self
end
type Vector = typeof(setmetatable({}, vectorMt))
local a: Vector = setmetatable({}, vectorMt)
local b = a + 2
local c = 2 + a
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("Vector", toString(requireType("a")));
CHECK_EQ(*requireType("a"), *requireType("b"));
CHECK_EQ(*requireType("a"), *requireType("c"));
}
// This test exposed a bug where we let go of the "seen" stack while unifying table types
// As a result, type inference crashed with a stack overflow.
TEST_CASE_FIXTURE(BuiltinsFixture, "unification_of_unions_in_a_self_referential_type")
@ -1570,7 +1551,7 @@ TEST_CASE_FIXTURE(Fixture, "right_table_missing_key2")
LUAU_REQUIRE_ERROR_COUNT(1, result);
MissingProperties* mp = get<MissingProperties>(result.errors[0]);
REQUIRE(mp);
REQUIRE_MESSAGE(mp, "Expected MissingProperties but got " << toString(result.errors[0]));
CHECK_EQ(mp->context, MissingProperties::Missing);
REQUIRE_EQ(1, mp->properties.size());
CHECK_EQ(mp->properties[0], "a");
@ -1664,7 +1645,7 @@ TEST_CASE_FIXTURE(Fixture, "casting_tables_with_props_into_table_with_indexer4")
if (FFlag::DebugLuauDeferredConstraintResolution)
{
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK(toString(result.errors[0]) == "Type 'string' could not be converted into 'number'");
CHECK(toString(result.errors[0]) == "Type 'number' could not be converted into 'string' in an invariant context");
}
else
{
@ -1685,7 +1666,7 @@ TEST_CASE_FIXTURE(Fixture, "table_subtyping_with_missing_props_dont_report_multi
if (FFlag::DebugLuauDeferredConstraintResolution)
{
CHECK_EQ("Type pack '{ x: number }' could not be converted into '{ x: number, y: number, z: number }'"
CHECK_EQ("Type pack '{ x: number }' could not be converted into '{ x: number, y: number, z: number }';"
" at [0], { x: number } is not a subtype of { x: number, y: number, z: number }",
toString(result.errors[0]));
}
@ -2176,7 +2157,7 @@ local b: B = a
LUAU_REQUIRE_ERRORS(result);
if (FFlag::DebugLuauDeferredConstraintResolution)
CHECK(toString(result.errors.at(0)) == R"(Type 'a' could not be converted into 'B'; at ["y"], number is not exactly string)");
CHECK(toString(result.errors.at(0)) == R"(Type 'a' could not be converted into 'B'; at [read "y"], number is not exactly string)");
else
{
const std::string expected = R"(Type 'A' could not be converted into 'B'
@ -2203,7 +2184,7 @@ local b: B = a
LUAU_REQUIRE_ERRORS(result);
if (FFlag::DebugLuauDeferredConstraintResolution)
CHECK(toString(result.errors.at(0)) == R"(Type 'a' could not be converted into 'B'; at ["b"]["y"], number is not exactly string)");
CHECK(toString(result.errors.at(0)) == R"(Type 'a' could not be converted into 'B'; at [read "b"][read "y"], number is not exactly string)");
else
{
const std::string expected = R"(Type 'A' could not be converted into 'B'
@ -3979,15 +3960,18 @@ TEST_CASE_FIXTURE(Fixture, "identify_all_problematic_table_fields")
LUAU_REQUIRE_ERROR_COUNT(1, result);
std::string expected = "Type 'a' could not be converted into 'T'; at [\"a\"], string is not exactly number"
"\n\tat [\"b\"], boolean is not exactly string"
"\n\tat [\"c\"], number is not exactly boolean";
std::string expected = "Type 'a' could not be converted into 'T'; at [read \"a\"], string is not exactly number"
"\n\tat [read \"b\"], boolean is not exactly string"
"\n\tat [read \"c\"], number is not exactly boolean";
CHECK(toString(result.errors[0]) == expected);
}
TEST_CASE_FIXTURE(Fixture, "read_and_write_only_table_properties_are_unsupported")
{
ScopedFastFlag sff{FFlag::LuauReadWritePropertySyntax, true};
ScopedFastFlag sff[] = {
{FFlag::LuauReadWritePropertySyntax, true},
{FFlag::DebugLuauDeferredConstraintResolution, false},
};
CheckResult result = check(R"(
type W = {read x: number}
@ -4026,6 +4010,22 @@ TEST_CASE_FIXTURE(Fixture, "read_ond_write_only_indexers_are_unsupported")
CHECK(Location{{2, 18}, {2, 23}} == result.errors[1].location);
}
TEST_CASE_FIXTURE(Fixture, "infer_write_property")
{
ScopedFastFlag sff{FFlag::DebugLuauDeferredConstraintResolution, true};
CheckResult result = check(R"(
function f(t)
t.y = 1
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
// CHECK("({ y: number }) -> ()" == toString(requireType("f")));
CHECK("({ y: number & unknown }) -> ()" == toString(requireType("f")));
}
TEST_CASE_FIXTURE(Fixture, "table_subtyping_error_suppression")
{
CheckResult result = check(R"(
@ -4057,4 +4057,132 @@ TEST_CASE_FIXTURE(Fixture, "table_subtyping_error_suppression")
}
}
TEST_CASE_FIXTURE(Fixture, "write_to_read_only_property")
{
ScopedFastFlag sff{FFlag::DebugLuauDeferredConstraintResolution, true};
CheckResult result = check(R"(
function f(t: {read x: number})
t.x = 5
end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK("Property x of table '{ read x: number }' is read-only" == toString(result.errors[0]));
PropertyAccessViolation* pav = get<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();

View file

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

View file

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

View file

@ -293,6 +293,24 @@ end
assert(loopIteratorProtocol(0, table.create(100, 5)) == 5058)
function valueTrackingIssue1()
local b = buffer.create(1)
buffer.writeu8(b, 0, 0)
local v1
local function closure()
assert(type(b) == "buffer") -- b is the first upvalue
v1 = nil -- v1 is the second upvalue
-- prevent inlining
for i = 1, 100 do print(`{b} is {b}`) end
end
closure()
end
valueTrackingIssue1()
local function vec3compsum(a: vector)
return a.X + a.Y + a.Z
end

View file

@ -1,4 +1,3 @@
AnnotationTests.typeof_expr
AstQuery.last_argument_function_call_type
AutocompleteTest.anonymous_autofilled_generic_on_argument_type_pack_vararg
AutocompleteTest.anonymous_autofilled_generic_type_pack_vararg
@ -13,7 +12,6 @@ BuiltinTests.assert_removes_falsy_types_even_from_type_pack_tail_but_only_for_th
BuiltinTests.assert_returns_false_and_string_iff_it_knows_the_first_argument_cannot_be_truthy
BuiltinTests.bad_select_should_not_crash
BuiltinTests.coroutine_resume_anything_goes
BuiltinTests.debug_info_is_crazy
BuiltinTests.global_singleton_types_are_sealed
BuiltinTests.gmatch_capture_types
BuiltinTests.gmatch_capture_types2
@ -67,24 +65,12 @@ DefinitionTests.class_definition_string_props
DefinitionTests.declaring_generic_functions
DefinitionTests.definition_file_classes
Differ.equal_generictp_cyclic
Differ.equal_table_A_B_C
Differ.equal_table_cyclic_diamonds_unraveled
Differ.equal_table_kind_A
Differ.equal_table_kind_B
Differ.equal_table_kind_C
Differ.equal_table_kind_D
Differ.equal_table_measuring_tapes
Differ.equal_table_two_shifted_circles_are_not_different
Differ.generictp_normal
Differ.generictp_normal_2
Differ.left_cyclic_table_right_table_missing_property
Differ.left_cyclic_table_right_table_property_wrong
Differ.metatable_metamissing_left
Differ.metatable_metamissing_right
Differ.metatable_metanormal
Differ.negation
Differ.right_cyclic_table_left_table_property_wrong
Differ.table_left_circle_right_measuring_tape
FrontendTest.accumulate_cached_errors_in_consistent_order
FrontendTest.environments
FrontendTest.imported_table_modification_2
@ -94,47 +80,37 @@ FrontendTest.trace_requires_in_nonstrict_mode
GenericsTests.apply_type_function_nested_generics1
GenericsTests.better_mismatch_error_messages
GenericsTests.bound_tables_do_not_clone_original_fields
GenericsTests.check_generic_function
GenericsTests.check_generic_local_function
GenericsTests.check_mutual_generic_functions
GenericsTests.check_mutual_generic_functions_errors
GenericsTests.check_mutual_generic_functions_unannotated
GenericsTests.check_nested_generic_function
GenericsTests.check_recursive_generic_function
GenericsTests.correctly_instantiate_polymorphic_member_functions
GenericsTests.do_not_always_instantiate_generic_intersection_types
GenericsTests.do_not_infer_generic_functions
GenericsTests.dont_leak_generic_types
GenericsTests.dont_leak_inferred_generic_types
GenericsTests.dont_substitute_bound_types
GenericsTests.error_detailed_function_mismatch_generic_pack
GenericsTests.error_detailed_function_mismatch_generic_types
GenericsTests.factories_of_generics
GenericsTests.function_arguments_can_be_polytypes
GenericsTests.generic_argument_count_too_few
GenericsTests.generic_argument_count_too_many
GenericsTests.generic_factories
GenericsTests.generic_functions_dont_cache_type_parameters
GenericsTests.generic_functions_in_types
GenericsTests.generic_type_families_work_in_subtyping
GenericsTests.generic_type_pack_parentheses
GenericsTests.generic_type_pack_unification1
GenericsTests.generic_type_pack_unification2
GenericsTests.generic_type_pack_unification3
GenericsTests.higher_rank_polymorphism_should_not_accept_instantiated_arguments
GenericsTests.hof_subtype_instantiation_regression
GenericsTests.infer_generic_function
GenericsTests.infer_generic_function_function_argument
GenericsTests.infer_generic_function_function_argument_2
GenericsTests.infer_generic_function_function_argument_3
GenericsTests.infer_generic_function_function_argument_overloaded
GenericsTests.infer_generic_lib_function_function_argument
GenericsTests.infer_generic_local_function
GenericsTests.infer_generic_property
GenericsTests.infer_nested_generic_function
GenericsTests.inferred_local_vars_can_be_polytypes
GenericsTests.instantiated_function_argument_names
GenericsTests.local_vars_can_be_polytypes
GenericsTests.mutable_state_polymorphism
GenericsTests.no_stack_overflow_from_quantifying
GenericsTests.properties_can_be_instantiated_polytypes
GenericsTests.properties_can_be_polytypes
GenericsTests.quantify_functions_even_if_they_have_an_explicit_generic
GenericsTests.rank_N_types_via_typeof
GenericsTests.self_recursive_instantiated_param
GenericsTests.type_parameters_can_be_polytypes
GenericsTests.typefuns_sharing_types
@ -164,7 +140,6 @@ IntersectionTypes.overloadeded_functions_with_weird_typepacks_3
IntersectionTypes.overloadeded_functions_with_weird_typepacks_4
IntersectionTypes.table_write_sealed_indirect
IntersectionTypes.union_saturate_overloaded_functions
Linter.DeprecatedApiFenv
Linter.FormatStringTyped
Linter.TableOperationsIndexer
ModuleTests.clone_self_property
@ -210,16 +185,12 @@ RefinementTest.discriminate_from_isa_of_x
RefinementTest.discriminate_from_truthiness_of_x
RefinementTest.discriminate_tag
RefinementTest.discriminate_tag_with_implicit_else
RefinementTest.else_with_no_explicit_expression_should_also_refine_the_tagged_union
RefinementTest.fail_to_refine_a_property_of_subscript_expression
RefinementTest.falsiness_of_TruthyPredicate_narrows_into_nil
RefinementTest.function_call_with_colon_after_refining_not_to_be_nil
RefinementTest.globals_can_be_narrowed_too
RefinementTest.impossible_type_narrow_is_not_an_error
RefinementTest.index_on_a_refined_property
RefinementTest.isa_type_refinement_must_be_known_ahead_of_time
RefinementTest.luau_polyfill_isindexkey_refine_conjunction
RefinementTest.luau_polyfill_isindexkey_refine_conjunction_variant
RefinementTest.narrow_property_of_a_bounded_variable
RefinementTest.nonoptional_type_can_narrow_to_nil_if_sense_is_true
RefinementTest.not_t_or_some_prop_of_t
@ -245,7 +216,6 @@ TableTests.any_when_indexing_into_an_unsealed_table_with_no_indexer_in_nonstrict
TableTests.array_factory_function
TableTests.casting_tables_with_props_into_table_with_indexer2
TableTests.casting_tables_with_props_into_table_with_indexer3
TableTests.casting_tables_with_props_into_table_with_indexer4
TableTests.casting_unsealed_tables_with_props_into_table_with_indexer
TableTests.checked_prop_too_early
TableTests.cli_84607_missing_prop_in_array_or_dict
@ -258,12 +228,10 @@ TableTests.common_table_element_union_in_call
TableTests.common_table_element_union_in_call_tail
TableTests.common_table_element_union_in_prop
TableTests.confusing_indexing
TableTests.cyclic_shifted_tables
TableTests.disallow_indexing_into_an_unsealed_table_with_no_indexer_in_strict_mode
TableTests.dont_crash_when_setmetatable_does_not_produce_a_metatabletypevar
TableTests.dont_extend_unsealed_tables_in_rvalue_position
TableTests.dont_leak_free_table_props
TableTests.dont_quantify_table_that_belongs_to_outer_scope
TableTests.dont_suggest_exact_match_keys
TableTests.error_detailed_indexer_key
TableTests.error_detailed_indexer_value
@ -275,8 +243,8 @@ TableTests.generalize_table_argument
TableTests.generic_table_instantiation_potential_regression
TableTests.indexer_mismatch
TableTests.indexers_get_quantified_too
TableTests.indexing_from_a_table_should_prefer_properties_when_possible
TableTests.inequality_operators_imply_exactly_matching_types
TableTests.infer_indexer_from_array_like_table
TableTests.infer_indexer_from_its_variable_type_and_unifiable
TableTests.inferred_return_type_of_free_table
TableTests.instantiate_table_cloning_3
@ -286,7 +254,7 @@ TableTests.length_operator_intersection
TableTests.length_operator_non_table_union
TableTests.length_operator_union
TableTests.less_exponential_blowup_please
TableTests.meta_add_both_ways
TableTests.meta_add
TableTests.meta_add_inferred
TableTests.metatable_mismatch_should_fail
TableTests.missing_metatable_for_sealed_tables_do_not_get_inferred
@ -300,8 +268,6 @@ TableTests.oop_polymorphic
TableTests.open_table_unification_2
TableTests.pass_a_union_of_tables_to_a_function_that_requires_a_table
TableTests.pass_a_union_of_tables_to_a_function_that_requires_a_table_2
TableTests.pass_incompatible_union_to_a_generic_table_without_crashing
TableTests.passing_compatible_unions_to_a_generic_table_without_crashing
TableTests.persistent_sealed_table_is_immutable
TableTests.prop_access_on_key_whose_types_mismatches
TableTests.prop_access_on_unions_of_indexers_where_key_whose_types_mismatches
@ -320,12 +286,10 @@ TableTests.shared_selfs_from_free_param
TableTests.shared_selfs_through_metatables
TableTests.table_call_metamethod_basic
TableTests.table_call_metamethod_must_be_callable
TableTests.table_function_check_use_after_free
TableTests.table_param_width_subtyping_2
TableTests.table_param_width_subtyping_3
TableTests.table_simple_call
TableTests.table_subtyping_with_extra_props_dont_report_multiple_errors
TableTests.table_subtyping_with_missing_props_dont_report_multiple_errors
TableTests.table_subtyping_with_missing_props_dont_report_multiple_errors2
TableTests.table_unification_4
TableTests.table_unifies_into_map
@ -351,7 +315,6 @@ TryUnifyTests.members_of_failed_typepack_unification_are_unified_with_errorType
TryUnifyTests.result_of_failed_typepack_unification_is_constrained
TryUnifyTests.uninhabited_table_sub_anything
TryUnifyTests.uninhabited_table_sub_never
TryUnifyTests.variadics_should_use_reversed_properly
TypeAliases.dont_lose_track_of_PendingExpansionTypes_after_substitution
TypeAliases.generic_param_remap
TypeAliases.mismatched_generic_type_param
@ -376,10 +339,10 @@ TypeFamilyTests.internal_families_raise_errors
TypeFamilyTests.table_internal_families
TypeFamilyTests.type_families_inhabited_with_normalization
TypeFamilyTests.unsolvable_family
TypeInfer.be_sure_to_use_active_txnlog_when_evaluating_a_variadic_overload
TypeInfer.bidirectional_checking_of_callback_property
TypeInfer.check_type_infer_recursion_count
TypeInfer.checking_should_not_ice
TypeInfer.cli_39932_use_unifier_in_ensure_methods
TypeInfer.cli_50041_committing_txnlog_in_apollo_client_error
TypeInfer.dont_ice_when_failing_the_occurs_check
TypeInfer.dont_report_type_errors_within_an_AstExprError
@ -447,7 +410,6 @@ TypeInferFunctions.function_exprs_are_generalized_at_signature_scope_not_enclosi
TypeInferFunctions.function_is_supertype_of_concrete_functions
TypeInferFunctions.function_statement_sealed_table_assignment_through_indexer
TypeInferFunctions.generic_packs_are_not_variadic
TypeInferFunctions.higher_order_function_2
TypeInferFunctions.higher_order_function_4
TypeInferFunctions.improved_function_arg_mismatch_error_nonstrict
TypeInferFunctions.improved_function_arg_mismatch_errors
@ -469,7 +431,6 @@ TypeInferFunctions.other_things_are_not_related_to_function
TypeInferFunctions.param_1_and_2_both_takes_the_same_generic_but_their_arguments_are_incompatible
TypeInferFunctions.param_1_and_2_both_takes_the_same_generic_but_their_arguments_are_incompatible_2
TypeInferFunctions.record_matching_overload
TypeInferFunctions.regex_benchmark_string_format_minimization
TypeInferFunctions.report_exiting_without_return_nonstrict
TypeInferFunctions.return_type_by_overload
TypeInferFunctions.too_few_arguments_variadic
@ -492,8 +453,6 @@ TypeInferLoops.for_in_loop_with_custom_iterator
TypeInferLoops.for_in_loop_with_incompatible_args_to_iterator
TypeInferLoops.for_in_loop_with_next
TypeInferLoops.for_in_with_an_iterator_of_type_any
TypeInferLoops.for_in_with_generic_next
TypeInferLoops.for_in_with_just_one_iterator_is_ok
TypeInferLoops.for_loop
TypeInferLoops.ipairs_produces_integral_indices
TypeInferLoops.iterate_over_free_table
@ -511,7 +470,6 @@ TypeInferLoops.properly_infer_iteratee_is_a_free_table
TypeInferLoops.repeat_loop
TypeInferLoops.varlist_declared_by_for_in_loop_should_be_free
TypeInferLoops.while_loop
TypeInferModules.bound_free_table_export_is_ok
TypeInferModules.custom_require_global
TypeInferModules.do_not_modify_imported_types
TypeInferModules.do_not_modify_imported_types_5
@ -521,7 +479,6 @@ TypeInferOOP.cycle_between_object_constructor_and_alias
TypeInferOOP.dont_suggest_using_colon_rather_than_dot_if_another_overload_works
TypeInferOOP.dont_suggest_using_colon_rather_than_dot_if_it_wont_help_2
TypeInferOOP.dont_suggest_using_colon_rather_than_dot_if_not_defined_with_colon
TypeInferOOP.inferred_methods_of_free_tables_have_the_same_level_as_the_enclosing_table
TypeInferOOP.inferring_hundreds_of_self_calls_should_not_suffocate_memory
TypeInferOOP.methods_are_topologically_sorted
TypeInferOOP.object_constructor_can_refer_to_method_of_self
@ -547,7 +504,6 @@ TypeInferOperators.strict_binary_op_where_lhs_unknown
TypeInferOperators.typecheck_overloaded_multiply_that_is_an_intersection
TypeInferOperators.typecheck_overloaded_multiply_that_is_an_intersection_on_rhs
TypeInferOperators.typecheck_unary_len_error
TypeInferOperators.typecheck_unary_minus
TypeInferOperators.typecheck_unary_minus_error
TypeInferOperators.UnknownGlobalCompoundAssign
TypeInferPrimitives.CheckMethodsOfNumber
@ -564,7 +520,6 @@ TypePackTests.pack_tail_unification_check
TypePackTests.type_alias_backwards_compatible
TypePackTests.type_alias_default_type_errors
TypePackTests.type_alias_type_packs_import
TypePackTests.type_packs_with_tails_in_vararg_adjustment
TypePackTests.unify_variadic_tails_in_arguments
TypeSingletons.enums_using_singletons_mismatch
TypeSingletons.error_detailed_tagged_union_mismatch_bool

View file

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

View file

@ -107,12 +107,6 @@ def main():
action="store_true",
help="Write a new faillist.txt after running tests.",
)
parser.add_argument(
"--rwp",
dest="rwp",
action="store_true",
help="Run the tests with read-write properties enabled.",
)
parser.add_argument(
"--ts",
dest="suite",
@ -135,8 +129,6 @@ def main():
failList = loadFailList()
flags = ["true", "DebugLuauDeferredConstraintResolution"]
if args.rwp:
flags.append("DebugLuauReadWriteProperties")
commandLine = [args.path, "--reporters=xml", "--fflags=" + ",".join(flags)]