Merge branch 'upstream' into merge

This commit is contained in:
Vighnesh 2024-02-23 11:12:27 -08:00
commit 0ab33af5c2
63 changed files with 1851 additions and 744 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,27 +1961,30 @@ Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprFunction* fun
{
Checkpoint startCheckpoint = checkpoint(this);
FunctionSignature sig = checkFunctionSignature(scope, func, expectedType);
interiorTypes.push_back(std::vector<TypeId>{});
checkFunctionBody(sig.bodyScope, func);
Checkpoint endCheckpoint = checkpoint(this);
TypeId generalizedTy = arena->addType(BlockedType{});
NotNull<Constraint> gc = addConstraint(sig.signatureScope, func->location, GeneralizationConstraint{generalizedTy, sig.signature, std::move(interiorTypes.back())});
interiorTypes.pop_back();
Constraint* previous = nullptr;
forEachConstraint(startCheckpoint, endCheckpoint, this, [gc, &previous](const ConstraintPtr& constraint) {
gc->dependencies.emplace_back(constraint.get());
if (auto psc = get<PackSubtypeConstraint>(*constraint); psc && psc->returns)
{
if (previous)
constraint->dependencies.push_back(NotNull{previous});
previous = constraint.get();
}
});
if (generalize && hasFreeType(sig.signature))
{
TypeId generalizedTy = arena->addType(BlockedType{});
NotNull<Constraint> gc = addConstraint(sig.signatureScope, func->location, GeneralizationConstraint{generalizedTy, sig.signature});
Constraint* previous = nullptr;
forEachConstraint(startCheckpoint, endCheckpoint, this, [gc, &previous](const ConstraintPtr& constraint) {
gc->dependencies.emplace_back(constraint.get());
if (auto psc = get<PackSubtypeConstraint>(*constraint); psc && psc->returns)
{
if (previous)
constraint->dependencies.push_back(NotNull{previous});
previous = constraint.get();
}
});
return Inference{generalizedTy};
}
else
@ -2529,7 +2548,8 @@ TypeId ConstraintGenerator::updateProperty(const ScopePtr& scope, AstExpr* expr,
{
TypeId segmentTy = arena->addType(BlockedType{});
module->astTypes[exprs[i]] = segmentTy;
addConstraint(scope, expr->location, HasPropConstraint{segmentTy, prevSegmentTy, segments[i]});
ValueContext ctx = i == segments.size() - 1 ? ValueContext::LValue : ValueContext::RValue;
addConstraint(scope, expr->location, HasPropConstraint{segmentTy, prevSegmentTy, segments[i], ctx});
prevSegmentTy = segmentTy;
}
@ -2563,6 +2583,8 @@ Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprTable* expr,
ttv->state = TableState::Unsealed;
ttv->scope = scope.get();
interiorTypes.back().push_back(ty);
auto createIndexer = [this, scope, ttv](const Location& location, TypeId currentIndexType, TypeId currentResultType) {
if (!ttv->indexer)
{
@ -2613,7 +2635,8 @@ Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprTable* expr,
{
expectedValueType = arena->addType(BlockedType{});
addConstraint(scope, item.value->location,
HasPropConstraint{*expectedValueType, *expectedType, stringKey->value.data, /*suppressSimplification*/ true});
HasPropConstraint{
*expectedValueType, *expectedType, stringKey->value.data, ValueContext::RValue, /*suppressSimplification*/ true});
}
}
}
@ -2876,15 +2899,10 @@ ConstraintGenerator::FunctionSignature ConstraintGenerator::checkFunctionSignatu
void ConstraintGenerator::checkFunctionBody(const ScopePtr& scope, AstExprFunction* fn)
{
visitBlockWithoutChildScope(scope, fn->body);
// If it is possible for execution to reach the end of the function, the return type must be compatible with ()
if (nullptr != getFallthrough(fn->body))
{
TypePackId empty = arena->addTypePack({}); // TODO we could have CG retain one of these forever
addConstraint(scope, fn->location, PackSubtypeConstraint{scope->returnType, empty});
}
ControlFlow cf = visitBlockWithoutChildScope(scope, fn->body);
if (cf == ControlFlow::None)
addConstraint(scope, fn->location, PackSubtypeConstraint{builtinTypes->emptyTypePack, scope->returnType});
}
TypeId ConstraintGenerator::resolveType(const ScopePtr& scope, AstType* ty, bool inTypeArguments, bool replaceErrorWithFresh)
@ -2977,20 +2995,30 @@ TypeId ConstraintGenerator::resolveType(const ScopePtr& scope, AstType* ty, bool
for (const AstTableProp& prop : tab->props)
{
if (prop.access == AstTableAccess::Read)
reportError(prop.accessLocation.value_or(Location{}), GenericError{"read keyword is illegal here"});
else if (prop.access == AstTableAccess::Write)
reportError(prop.accessLocation.value_or(Location{}), GenericError{"write keyword is illegal here"});
else if (prop.access == AstTableAccess::ReadWrite)
// TODO: Recursion limit.
TypeId propTy = resolveType(scope, prop.type, inTypeArguments);
Property& p = props[prop.name.value];
p.typeLocation = prop.location;
switch (prop.access)
{
std::string name = prop.name.value;
// TODO: Recursion limit.
TypeId propTy = resolveType(scope, prop.type, inTypeArguments);
props[name] = {propTy};
props[name].typeLocation = prop.location;
}
else
case AstTableAccess::ReadWrite:
p.readTy = propTy;
p.writeTy = propTy;
break;
case AstTableAccess::Read:
p.readTy = propTy;
break;
case AstTableAccess::Write:
reportError(*prop.accessLocation, GenericError{"write keyword is illegal here"});
p.readTy = propTy;
p.writeTy = propTy;
break;
default:
ice->ice("Unexpected property access " + std::to_string(int(prop.access)));
break;
}
}
if (AstTableIndexer* astIndexer = tab->indexer)

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);
@ -2055,34 +2136,39 @@ std::pair<std::vector<TypeId>, std::optional<TypeId>> ConstraintSolver::lookupTa
return {{}, std::nullopt};
}
template<typename TID>
bool ConstraintSolver::tryUnify(NotNull<const Constraint> constraint, TID subTy, TID superTy)
template <typename TID>
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,12 +2349,61 @@ 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
prop.setType(intersectionType(hprop.type(), tprop.type()));
hereSubThere &= (prop.type() == hprop.type());
thereSubHere &= (prop.type() == tprop.type());
if (FFlag::DebugLuauDeferredConstraintResolution)
{
if (hprop.readTy.has_value())
{
if (tprop.readTy.has_value())
{
TypeId ty = simplifyIntersection(builtinTypes, NotNull{arena}, *hprop.readTy, *tprop.readTy).result;
prop.readTy = ty;
hereSubThere &= (ty == hprop.readTy);
thereSubHere &= (ty == tprop.readTy);
}
else
{
prop.readTy = *hprop.readTy;
thereSubHere = false;
}
}
else if (tprop.readTy.has_value())
{
prop.readTy = *tprop.readTy;
hereSubThere = false;
}
if (hprop.writeTy.has_value())
{
if (tprop.writeTy.has_value())
{
prop.writeTy = simplifyIntersection(builtinTypes, NotNull{arena}, *hprop.writeTy, *tprop.writeTy).result;
hereSubThere &= (prop.writeTy == hprop.writeTy);
thereSubHere &= (prop.writeTy == tprop.writeTy);
}
else
{
prop.writeTy = *hprop.writeTy;
thereSubHere = false;
}
}
else if (tprop.writeTy.has_value())
{
prop.writeTy = *tprop.writeTy;
hereSubThere = false;
}
}
else
{
prop.setType(intersectionType(hprop.type(), tprop.type()));
hereSubThere &= (prop.type() == hprop.type());
thereSubHere &= (prop.type() == tprop.type());
}
}
// TODO: string indexers
result.props[name] = prop;
if (prop.readTy || prop.writeTy)
result.props[name] = prop;
}
for (const auto& [name, tprop] : tttv->props)
@ -2431,8 +2480,10 @@ void Normalizer::intersectTablesWithTable(TypeIds& heres, TypeId there)
{
TypeIds tmp;
for (TypeId here : heres)
{
if (std::optional<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 foundPropFromRightInLeft = std::any_of(begin(rightTable->props), end(rightTable->props), [&](auto prop) {
return leftTable->props.find(prop.first) != end(leftTable->props);
});
bool foundPropFromLeftInRight = std::any_of(begin(leftTable->props), end(leftTable->props),
[&](auto prop)
{
return rightTable->props.count(prop.first) > 0;
});
bool foundPropFromRightInLeft = std::any_of(begin(rightTable->props), end(rightTable->props),
[&](auto prop)
{
return leftTable->props.count(prop.first) > 0;
});
if (!(foundPropFromLeftInRight || foundPropFromRightInLeft) && leftTable->props.size() >= 1 && rightTable->props.size() >= 1)
if (!foundPropFromLeftInRight && !foundPropFromRightInLeft && leftTable->props.size() >= 1 && rightTable->props.size() >= 1)
return Relation::Disjoint;
const auto [propName, rightProp] = *begin(rightTable->props);
@ -257,7 +263,10 @@ Relation relateTables(TypeId left, TypeId right)
const Property leftProp = it->second;
Relation r = relate(leftProp.type(), rightProp.type());
if (!leftProp.isShared() || !rightProp.isShared())
return Relation::Intersects;
Relation r = relate(leftProp.type(), rightProp.type(), seen);
if (r == Relation::Coincident && 1 != leftTable->props.size())
{
// eg {tag: "cat", prop: string} & {tag: "cat"}
@ -268,7 +277,7 @@ Relation relateTables(TypeId left, TypeId right)
}
// A cheap and approximate subtype test
Relation relate(TypeId left, TypeId right)
Relation relate(TypeId left, TypeId right, SimplifierSeenSet& seen)
{
// TODO nice to have: Relate functions of equal argument and return arity
@ -278,6 +287,14 @@ Relation relate(TypeId left, TypeId right)
if (left == right)
return Relation::Coincident;
std::pair<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,9 +1290,12 @@ TypeId TypeSimplifier::simplify(TypeId ty, DenseHashSet<TypeId>& seen)
{
if (1 == tt->props.size())
{
TypeId propTy = simplify(begin(tt->props)->second.type(), seen);
if (get<NeverType>(propTy))
return builtinTypes->neverType;
if (std::optional<TypeId> readTy = begin(tt->props)->second.readTy)
{
TypeId propTy = simplify(*readTy, seen);
if (get<NeverType>(propTy))
return builtinTypes->neverType;
}
}
}

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())
.withSubComponent(TypePath::TypeField::IndexResult)
.withSuperComponent(TypePath::Property(name)));
if (isCovariantWith(env, builtinTypes->stringType, subTable->indexer->indexType).isSubtype)
{
if (superProp.isShared())
results.push_back(isInvariantWith(env, subTable->indexer->indexResultType, superProp.type())
.withSubComponent(TypePath::TypeField::IndexResult)
.withSuperComponent(TypePath::Property::read(name)));
else
{
if (superProp.readTy)
results.push_back(isCovariantWith(env, subTable->indexer->indexResultType, *superProp.readTy)
.withSubComponent(TypePath::TypeField::IndexResult)
.withSuperComponent(TypePath::Property::read(name)));
if (superProp.writeTy)
results.push_back(isContravariantWith(env, subTable->indexer->indexResultType, *superProp.writeTy)
.withSubComponent(TypePath::TypeField::IndexResult)
.withSuperComponent(TypePath::Property::write(name)));
}
}
}
if (results.empty())
@ -1197,7 +1308,9 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const Clas
for (const auto& [name, prop] : superTable->props)
{
if (auto classProp = lookupClassProp(subClass, name))
result.andAlso(isInvariantWith(env, prop.type(), classProp->type()).withBothComponent(TypePath::Property(name)));
{
result.andAlso(isCovariantWith(env, *classProp, prop, name));
}
else
return SubtypingResult{false};
}
@ -1230,7 +1343,7 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const Prim
{
if (auto stringTable = get<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,31 +385,46 @@ struct TypeStringifier
state.emit("\"]");
}
state.emit(": ");
}
if (FFlag::DebugLuauReadWriteProperties)
void _newStringify(const std::string& name, const Property& prop)
{
bool comma = false;
if (prop.isShared())
{
// We special case the stringification if the property's read and write types are shared.
if (prop.isShared())
return stringify(*prop.readType());
// Otherwise emit them separately.
if (auto ty = prop.readType())
{
state.emit("read ");
stringify(*ty);
}
if (prop.readType() && prop.writeType())
state.emit(" + ");
if (auto ty = prop.writeType())
{
state.emit("write ");
stringify(*ty);
}
}
else
emitKey(name);
stringify(prop.type());
return;
}
if (prop.readTy)
{
state.emit("read ");
emitKey(name);
stringify(*prop.readTy);
comma = true;
}
if (prop.writeTy)
{
if (comma)
{
state.emit(",");
state.newline();
}
state.emit("write ");
emitKey(name);
stringify(*prop.writeTy);
}
}
void stringify(const std::string& name, const Property& prop)
{
if (FFlag::DebugLuauDeferredConstraintResolution)
return _newStringify(name, prop);
emitKey(name);
stringify(prop.type());
}
void stringify(TypePackId tp);
@ -1755,7 +1770,7 @@ std::string toString(const Constraint& constraint, ToStringOptions& opts)
}
else if constexpr (std::is_same_v<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,7 +58,20 @@ std::optional<TypeId> findTablePropertyRespectingMeta(
{
const auto& it = tableType->props.find(name);
if (it != tableType->props.end())
return it->second.type();
{
if (FFlag::DebugLuauDeferredConstraintResolution)
{
switch (context)
{
case ValueContext::RValue:
return it->second.readTy;
case ValueContext::LValue:
return it->second.writeTy;
}
}
else
return it->second.type();
}
}
std::optional<TypeId> mtIndex = findMetatableEntry(builtinTypes, errors, ty, "__index", location);

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)
result &= unify(subOption, superTy);
{
if (areCompatible(subOption, superTy))
result &= unify(subOption, superTy);
}
return result;
}
@ -190,7 +258,10 @@ bool Unifier2::unify(TypeId subTy, const UnionType* superUnion)
// if the occurs check fails for any option, it fails overall
for (auto superOption : superUnion->options)
result &= unify(subTy, superOption);
{
if (areCompatible(subTy, superOption))
result &= unify(subTy, superOption);
}
return result;
}
@ -228,7 +299,21 @@ bool Unifier2::unify(TableType* subTable, const TableType* superTable)
auto superPropOpt = superTable->props.find(propName);
if (superPropOpt != superTable->props.end())
result &= unify(subProp.type(), superPropOpt->second.type());
{
const Property& superProp = superPropOpt->second;
if (subProp.isReadOnly() && superProp.isReadOnly())
result &= unify(*subProp.readTy, *superPropOpt->second.readTy);
else if (subProp.isReadOnly())
result &= unify(*subProp.readTy, superProp.type());
else if (superProp.isReadOnly())
result &= unify(subProp.type(), *superProp.readTy);
else
{
result &= unify(subProp.type(), superProp.type());
result &= unify(superProp.type(), subProp.type());
}
}
}
auto subTypeParamsIter = subTable->instantiatedTypeParams.begin();
@ -293,10 +378,19 @@ bool Unifier2::unify(TypePackId subTp, TypePackId superTp)
subTp = follow(subTp);
superTp = follow(superTp);
if (auto subGen = genericPackSubstitutions.find(subTp))
return unify(*subGen, superTp);
if (auto superGen = genericPackSubstitutions.find(superTp))
return unify(subTp, *superGen);
if (seenTypePackPairings.contains({subTp, superTp}))
return true;
seenTypePackPairings.insert({subTp, superTp});
if (subTp == superTp)
return true;
const FreeTypePack* subFree = get<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

@ -63,13 +63,22 @@ AssemblyBuilderA64::~AssemblyBuilderA64()
void AssemblyBuilderA64::mov(RegisterA64 dst, RegisterA64 src)
{
CODEGEN_ASSERT(dst.kind == KindA64::w || dst.kind == KindA64::x || dst == sp);
CODEGEN_ASSERT(dst.kind == src.kind || (dst.kind == KindA64::x && src == sp) || (dst == sp && src.kind == KindA64::x));
if (dst.kind != KindA64::q)
{
CODEGEN_ASSERT(dst.kind == KindA64::w || dst.kind == KindA64::x || dst == sp);
CODEGEN_ASSERT(dst.kind == src.kind || (dst.kind == KindA64::x && src == sp) || (dst == sp && src.kind == KindA64::x));
if (dst == sp || src == sp)
placeR1("mov", dst, src, 0b00'100010'0'000000000000);
else
placeSR2("mov", dst, src, 0b01'01010);
}
else
{
CODEGEN_ASSERT(dst.kind == src.kind);
placeR1("mov", dst, src, 0b10'01110'10'1'00000'00011'1 | (src.index << 6));
}
}
void AssemblyBuilderA64::mov(RegisterA64 dst, int src)
@ -577,7 +586,7 @@ void AssemblyBuilderA64::fadd(RegisterA64 dst, RegisterA64 src1, RegisterA64 src
}
else if (dst.kind == KindA64::s)
{
CODEGEN_ASSERT(dst.kind == KindA64::s && src1.kind == KindA64::s && src2.kind == KindA64::s);
CODEGEN_ASSERT(src1.kind == KindA64::s && src2.kind == KindA64::s);
placeR3("fadd", dst, src1, src2, 0b11110'00'1, 0b0010'10);
}
@ -599,7 +608,7 @@ void AssemblyBuilderA64::fdiv(RegisterA64 dst, RegisterA64 src1, RegisterA64 src
}
else if (dst.kind == KindA64::s)
{
CODEGEN_ASSERT(dst.kind == KindA64::s && src1.kind == KindA64::s && src2.kind == KindA64::s);
CODEGEN_ASSERT(src1.kind == KindA64::s && src2.kind == KindA64::s);
placeR3("fdiv", dst, src1, src2, 0b11110'00'1, 0b0001'10);
}
@ -621,7 +630,7 @@ void AssemblyBuilderA64::fmul(RegisterA64 dst, RegisterA64 src1, RegisterA64 src
}
else if (dst.kind == KindA64::s)
{
CODEGEN_ASSERT(dst.kind == KindA64::s && src1.kind == KindA64::s && src2.kind == KindA64::s);
CODEGEN_ASSERT(src1.kind == KindA64::s && src2.kind == KindA64::s);
placeR3("fmul", dst, src1, src2, 0b11110'00'1, 0b0000'10);
}
@ -643,7 +652,7 @@ void AssemblyBuilderA64::fneg(RegisterA64 dst, RegisterA64 src)
}
else if (dst.kind == KindA64::s)
{
CODEGEN_ASSERT(dst.kind == KindA64::s && src.kind == KindA64::s);
CODEGEN_ASSERT(src.kind == KindA64::s);
placeR1("fneg", dst, src, 0b000'11110'00'1'0000'10'10000);
}
@ -672,7 +681,7 @@ void AssemblyBuilderA64::fsub(RegisterA64 dst, RegisterA64 src1, RegisterA64 src
}
else if (dst.kind == KindA64::s)
{
CODEGEN_ASSERT(dst.kind == KindA64::s && src1.kind == KindA64::s && src2.kind == KindA64::s);
CODEGEN_ASSERT(src1.kind == KindA64::s && src2.kind == KindA64::s);
placeR3("fsub", dst, src1, src2, 0b11110'00'1, 0b0011'10);
}
@ -982,18 +991,6 @@ void AssemblyBuilderA64::placeR3(const char* name, RegisterA64 dst, RegisterA64
commit();
}
void AssemblyBuilderA64::placeR3(const char* name, RegisterA64 dst, RegisterA64 src1, RegisterA64 src2, uint8_t sizes, uint8_t op, uint8_t op2)
{
if (logText)
log(name, dst, src1, src2);
CODEGEN_ASSERT(dst.kind == KindA64::w || dst.kind == KindA64::x || dst.kind == KindA64::d || dst.kind == KindA64::q);
CODEGEN_ASSERT(dst.kind == src1.kind && dst.kind == src2.kind);
place(dst.index | (src1.index << 5) | (op2 << 10) | (src2.index << 16) | (op << 21) | (sizes << 29));
commit();
}
void AssemblyBuilderA64::placeR1(const char* name, RegisterA64 dst, RegisterA64 src, uint32_t op)
{
if (logText)

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,12 +1419,13 @@ TEST_CASE_FIXTURE(SubtypeFixture, "multiple_reasonings")
SubtypingResult result = isSubtype(subTy, superTy);
CHECK(!result.isSubtype);
CHECK(result.reasoning == std::vector{
SubtypingReasoning{/* subPath */ Path(TypePath::Property("X")), /* superPath */ Path(TypePath::Property("X")),
/* variance */ SubtypingVariance::Invariant},
SubtypingReasoning{/* subPath */ Path(TypePath::Property("Y")), /* superPath */ Path(TypePath::Property("Y")),
/* variance */ SubtypingVariance::Invariant},
});
CHECK(result.reasoning ==
std::vector{
SubtypingReasoning{/* subPath */ Path(TypePath::Property::read("X")), /* superPath */ Path(TypePath::Property::read("X")),
/* variance */ SubtypingVariance::Invariant},
SubtypingReasoning{/* subPath */ Path(TypePath::Property::read("Y")), /* superPath */ Path(TypePath::Property::read("Y")),
/* variance */ SubtypingVariance::Invariant},
});
}
TEST_SUITE_END();

View file

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

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

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