Sync to upstream/release/660 (#1643)

# General

This release introduces initial work on a Roundtrippable AST for Luau,
and numerous fixes to the new type solver, runtime, and fragment
autocomplete.

## Roundtrippable AST

To support tooling around source code transformations, we are extending
the parser to retain source information so that we can re-emit the
initial source code exactly as the author wrote it. We have made
numerous changes to the Transpiler, added new AST types such as
`AstTypeGroup`, and added source information to AST nodes such as
`AstExprInterpString`, `AstExprIfElse`, `AstTypeTable`,
`AstTypeReference`, `AstTypeSingletonString`, and `AstTypeTypeof`.

## New Type Solver

* Implement `setmetatable` and `getmetatable` type functions.
* Fix handling of nested and recursive union type functions to prevent
the solver from getting stuck.
* Free types in both old and new solver now have an upper and lower
bound to resolve mixed mode usage of the solvers in fragment
autocomplete.
* Fix infinite recursion during normalization of cyclic tables.
* Add normalization support for intersections of subclasses with negated
superclasses.

## Runtime
* Fix compilation error in Luau buffer bit operations for big-endian
machines.

## Miscellaneous
* Add test and bugfixes to fragment autocomplete.
* Fixed `clang-tidy` warnings in `Simplify.cpp`.

**Full Changelog**:
https://github.com/luau-lang/luau/compare/0.659...0.660

---

Co-authored-by: Ariel Weiss <aaronweiss@roblox.com>
Co-authored-by: Hunter Goldstein <hgoldstein@roblox.com>
Co-authored-by: Talha Pathan <tpathan@roblox.com>
Co-authored-by: Varun Saini <vsaini@roblox.com>
Co-authored-by: Vighnesh Vijay <vvijay@roblox.com>
Co-authored-by: Vyacheslav Egorov <vegorov@roblox.com>

---------

Co-authored-by: Hunter Goldstein <hgoldstein@roblox.com>
Co-authored-by: Varun Saini <61795485+vrn-sn@users.noreply.github.com>
Co-authored-by: Alexander Youngblood <ayoungblood@roblox.com>
Co-authored-by: Menarul Alam <malam@roblox.com>
This commit is contained in:
Aviral Goel 2025-02-07 16:17:11 -08:00 committed by GitHub
parent f8a1e0129d
commit 2e61028cba
Signed by: DevComp
GPG key ID: B5690EEEBB952194
73 changed files with 5870 additions and 519 deletions

View file

@ -4,6 +4,7 @@
#include <Luau/NotNull.h>
#include "Luau/TypeArena.h"
#include "Luau/Type.h"
#include "Luau/Scope.h"
#include <unordered_map>
@ -26,13 +27,17 @@ struct CloneState
* while `clone` will make a deep copy of the entire type and its every component.
*
* Be mindful about which behavior you actually _want_.
*
* Persistent types are not cloned as an optimization.
* If a type is cloned in order to mutate it, 'ignorePersistent' has to be set
*/
TypePackId shallowClone(TypePackId tp, TypeArena& dest, CloneState& cloneState);
TypeId shallowClone(TypeId typeId, TypeArena& dest, CloneState& cloneState);
TypePackId shallowClone(TypePackId tp, TypeArena& dest, CloneState& cloneState, bool ignorePersistent = false);
TypeId shallowClone(TypeId typeId, TypeArena& dest, CloneState& cloneState, bool ignorePersistent = false);
TypePackId clone(TypePackId tp, TypeArena& dest, CloneState& cloneState);
TypeId clone(TypeId tp, TypeArena& dest, CloneState& cloneState);
TypeFun clone(const TypeFun& typeFun, TypeArena& dest, CloneState& cloneState);
Binding clone(const Binding& binding, TypeArena& dest, CloneState& cloneState);
} // namespace Luau

View file

@ -7,6 +7,7 @@
#include "Luau/ModuleResolver.h"
#include "Luau/RequireTracer.h"
#include "Luau/Scope.h"
#include "Luau/Set.h"
#include "Luau/TypeCheckLimits.h"
#include "Luau/Variant.h"
#include "Luau/AnyTypeSummary.h"
@ -56,13 +57,32 @@ struct SourceNode
return forAutocomplete ? dirtyModuleForAutocomplete : dirtyModule;
}
bool hasInvalidModuleDependency(bool forAutocomplete) const
{
return forAutocomplete ? invalidModuleDependencyForAutocomplete : invalidModuleDependency;
}
void setInvalidModuleDependency(bool value, bool forAutocomplete)
{
if (forAutocomplete)
invalidModuleDependencyForAutocomplete = value;
else
invalidModuleDependency = value;
}
ModuleName name;
std::string humanReadableName;
DenseHashSet<ModuleName> requireSet{{}};
std::vector<std::pair<ModuleName, Location>> requireLocations;
Set<ModuleName> dependents{{}};
bool dirtySourceModule = true;
bool dirtyModule = true;
bool dirtyModuleForAutocomplete = true;
bool invalidModuleDependency = true;
bool invalidModuleDependencyForAutocomplete = true;
double autocompleteLimitsMult = 1.0;
};
@ -117,7 +137,7 @@ struct FrontendModuleResolver : ModuleResolver
std::optional<ModuleInfo> resolveModuleInfo(const ModuleName& currentModuleName, const AstExpr& pathExpr) override;
std::string getHumanReadableModuleName(const ModuleName& moduleName) const override;
void setModule(const ModuleName& moduleName, ModulePtr module);
bool setModule(const ModuleName& moduleName, ModulePtr module);
void clearModules();
private:
@ -151,9 +171,13 @@ struct Frontend
// Parse and typecheck module graph
CheckResult check(const ModuleName& name, std::optional<FrontendOptions> optionOverride = {}); // new shininess
bool allModuleDependenciesValid(const ModuleName& name, bool forAutocomplete = false) const;
bool isDirty(const ModuleName& name, bool forAutocomplete = false) const;
void markDirty(const ModuleName& name, std::vector<ModuleName>* markedDirty = nullptr);
void traverseDependents(const ModuleName& name, std::function<bool(SourceNode&)> processSubtree);
/** Borrow a pointer into the SourceModule cache.
*
* Returns nullptr if we don't have it. This could mean that the script

View file

@ -19,10 +19,10 @@ struct SimplifyResult
DenseHashSet<TypeId> blockedTypes;
};
SimplifyResult simplifyIntersection(NotNull<BuiltinTypes> builtinTypes, NotNull<TypeArena> arena, TypeId ty, TypeId discriminant);
SimplifyResult simplifyIntersection(NotNull<BuiltinTypes> builtinTypes, NotNull<TypeArena> arena, TypeId left, TypeId right);
SimplifyResult simplifyIntersection(NotNull<BuiltinTypes> builtinTypes, NotNull<TypeArena> arena, std::set<TypeId> parts);
SimplifyResult simplifyUnion(NotNull<BuiltinTypes> builtinTypes, NotNull<TypeArena> arena, TypeId ty, TypeId discriminant);
SimplifyResult simplifyUnion(NotNull<BuiltinTypes> builtinTypes, NotNull<TypeArena> arena, TypeId left, TypeId right);
enum class Relation
{

View file

@ -69,12 +69,16 @@ using Name = std::string;
// A free type is one whose exact shape has yet to be fully determined.
struct FreeType
{
// New constructors
explicit FreeType(TypeLevel level, TypeId lowerBound, TypeId upperBound);
// This one got promoted to explicit
explicit FreeType(Scope* scope, TypeId lowerBound, TypeId upperBound);
explicit FreeType(Scope* scope, TypeLevel level, TypeId lowerBound, TypeId upperBound);
// Old constructors
explicit FreeType(TypeLevel level);
explicit FreeType(Scope* scope);
FreeType(Scope* scope, TypeLevel level);
FreeType(Scope* scope, TypeId lowerBound, TypeId upperBound);
int index;
TypeLevel level;
Scope* scope = nullptr;

View file

@ -32,9 +32,13 @@ struct TypeArena
TypeId addTV(Type&& tv);
TypeId freshType(TypeLevel level);
TypeId freshType(Scope* scope);
TypeId freshType(Scope* scope, TypeLevel level);
TypeId freshType(NotNull<BuiltinTypes> builtins, TypeLevel level);
TypeId freshType(NotNull<BuiltinTypes> builtins, Scope* scope);
TypeId freshType(NotNull<BuiltinTypes> builtins, Scope* scope, TypeLevel level);
TypeId freshType_DEPRECATED(TypeLevel level);
TypeId freshType_DEPRECATED(Scope* scope);
TypeId freshType_DEPRECATED(Scope* scope, TypeLevel level);
TypePackId freshTypePack(Scope* scope);

View file

@ -241,6 +241,9 @@ struct BuiltinTypeFunctions
TypeFunction indexFunc;
TypeFunction rawgetFunc;
TypeFunction setmetatableFunc;
TypeFunction getmetatableFunc;
void addToScope(NotNull<TypeArena> arena, NotNull<Scope> scope) const;
};

View file

@ -1161,6 +1161,19 @@ struct AstJsonEncoder : public AstVisitor
);
}
bool visit(class AstTypeGroup* node) override
{
writeNode(
node,
"AstTypeGroup",
[&]()
{
write("type", node->type);
}
);
return false;
}
bool visit(class AstTypeSingletonBool* node) override
{
writeNode(

View file

@ -29,12 +29,11 @@
*/
LUAU_FASTFLAG(LuauSolverV2)
LUAU_FASTFLAGVARIABLE(LuauTypestateBuiltins2)
LUAU_FASTFLAGVARIABLE(LuauStringFormatArityFix)
LUAU_FASTFLAGVARIABLE(LuauStringFormatErrorSuppression)
LUAU_FASTFLAG(AutocompleteRequirePathSuggestions2)
LUAU_FASTFLAGVARIABLE(LuauTableCloneClonesType2)
LUAU_FASTFLAGVARIABLE(LuauTableCloneClonesType3)
LUAU_FASTFLAG(LuauTrackInteriorFreeTypesOnScope)
LUAU_FASTFLAGVARIABLE(LuauFreezeIgnorePersistent)
namespace Luau
{
@ -449,9 +448,8 @@ void registerBuiltinGlobals(Frontend& frontend, GlobalTypes& globals, bool typeC
ttv->props["foreachi"].deprecated = true;
attachMagicFunction(ttv->props["pack"].type(), std::make_shared<MagicPack>());
if (FFlag::LuauTableCloneClonesType2)
if (FFlag::LuauTableCloneClonesType3)
attachMagicFunction(ttv->props["clone"].type(), std::make_shared<MagicClone>());
if (FFlag::LuauTypestateBuiltins2)
attachMagicFunction(ttv->props["freeze"].type(), std::make_shared<MagicFreeze>());
}
@ -613,10 +611,7 @@ bool MagicFormat::typeCheck(const MagicFunctionTypeCheckContext& context)
if (!fmt)
{
if (FFlag::LuauStringFormatArityFix)
context.typechecker->reportError(
CountMismatch{1, std::nullopt, 0, CountMismatch::Arg, true, "string.format"}, context.callSite->location
);
context.typechecker->reportError(CountMismatch{1, std::nullopt, 0, CountMismatch::Arg, true, "string.format"}, context.callSite->location);
return true;
}
@ -1401,7 +1396,7 @@ std::optional<WithPredicate<TypePackId>> MagicClone::handleOldSolver(
WithPredicate<TypePackId> withPredicate
)
{
LUAU_ASSERT(FFlag::LuauTableCloneClonesType2);
LUAU_ASSERT(FFlag::LuauTableCloneClonesType3);
auto [paramPack, _predicates] = withPredicate;
@ -1416,6 +1411,9 @@ std::optional<WithPredicate<TypePackId>> MagicClone::handleOldSolver(
TypeId inputType = follow(paramTypes[0]);
if (!get<TableType>(inputType))
return std::nullopt;
CloneState cloneState{typechecker.builtinTypes};
TypeId resultType = shallowClone(inputType, arena, cloneState);
@ -1425,7 +1423,7 @@ std::optional<WithPredicate<TypePackId>> MagicClone::handleOldSolver(
bool MagicClone::infer(const MagicFunctionCallContext& context)
{
LUAU_ASSERT(FFlag::LuauTableCloneClonesType2);
LUAU_ASSERT(FFlag::LuauTableCloneClonesType3);
TypeArena* arena = context.solver->arena;
@ -1438,8 +1436,11 @@ bool MagicClone::infer(const MagicFunctionCallContext& context)
TypeId inputType = follow(paramTypes[0]);
if (!get<TableType>(inputType))
return false;
CloneState cloneState{context.solver->builtinTypes};
TypeId resultType = shallowClone(inputType, *arena, cloneState);
TypeId resultType = shallowClone(inputType, *arena, cloneState, /* ignorePersistent */ FFlag::LuauFreezeIgnorePersistent);
if (auto tableType = getMutable<TableType>(resultType))
{
@ -1475,7 +1476,7 @@ static std::optional<TypeId> freezeTable(TypeId inputType, const MagicFunctionCa
{
// Clone the input type, this will become our final result type after we mutate it.
CloneState cloneState{context.solver->builtinTypes};
TypeId resultType = shallowClone(inputType, *arena, cloneState);
TypeId resultType = shallowClone(inputType, *arena, cloneState, /* ignorePersistent */ FFlag::LuauFreezeIgnorePersistent);
auto tableTy = getMutable<TableType>(resultType);
// `clone` should not break this.
LUAU_ASSERT(tableTy);
@ -1507,8 +1508,6 @@ std::optional<WithPredicate<TypePackId>> MagicFreeze::handleOldSolver(struct Typ
bool MagicFreeze::infer(const MagicFunctionCallContext& context)
{
LUAU_ASSERT(FFlag::LuauTypestateBuiltins2);
TypeArena* arena = context.solver->arena;
const DataFlowGraph* dfg = context.solver->dfg.get();
Scope* scope = context.constraint->scope.get();

View file

@ -7,6 +7,7 @@
#include "Luau/Unifiable.h"
LUAU_FASTFLAG(LuauSolverV2)
LUAU_FASTFLAG(LuauFreezeIgnorePersistent)
// For each `Luau::clone` call, we will clone only up to N amount of types _and_ packs, as controlled by this limit.
LUAU_FASTINTVARIABLE(LuauTypeCloneIterationLimit, 100'000)
@ -38,14 +39,26 @@ class TypeCloner
NotNull<SeenTypes> types;
NotNull<SeenTypePacks> packs;
TypeId forceTy = nullptr;
TypePackId forceTp = nullptr;
int steps = 0;
public:
TypeCloner(NotNull<TypeArena> arena, NotNull<BuiltinTypes> builtinTypes, NotNull<SeenTypes> types, NotNull<SeenTypePacks> packs)
TypeCloner(
NotNull<TypeArena> arena,
NotNull<BuiltinTypes> builtinTypes,
NotNull<SeenTypes> types,
NotNull<SeenTypePacks> packs,
TypeId forceTy,
TypePackId forceTp
)
: arena(arena)
, builtinTypes(builtinTypes)
, types(types)
, packs(packs)
, forceTy(forceTy)
, forceTp(forceTp)
{
}
@ -112,7 +125,7 @@ private:
ty = follow(ty, FollowOption::DisableLazyTypeThunks);
if (auto it = types->find(ty); it != types->end())
return it->second;
else if (ty->persistent)
else if (ty->persistent && (!FFlag::LuauFreezeIgnorePersistent || ty != forceTy))
return ty;
return std::nullopt;
}
@ -122,7 +135,7 @@ private:
tp = follow(tp);
if (auto it = packs->find(tp); it != packs->end())
return it->second;
else if (tp->persistent)
else if (tp->persistent && (!FFlag::LuauFreezeIgnorePersistent || tp != forceTp))
return tp;
return std::nullopt;
}
@ -148,7 +161,7 @@ public:
if (auto clone = find(ty))
return *clone;
else if (ty->persistent)
else if (ty->persistent && (!FFlag::LuauFreezeIgnorePersistent || ty != forceTy))
return ty;
TypeId target = arena->addType(ty->ty);
@ -174,7 +187,7 @@ public:
if (auto clone = find(tp))
return *clone;
else if (tp->persistent)
else if (tp->persistent && (!FFlag::LuauFreezeIgnorePersistent || tp != forceTp))
return tp;
TypePackId target = arena->addTypePack(tp->ty);
@ -458,21 +471,37 @@ private:
} // namespace
TypePackId shallowClone(TypePackId tp, TypeArena& dest, CloneState& cloneState)
TypePackId shallowClone(TypePackId tp, TypeArena& dest, CloneState& cloneState, bool ignorePersistent)
{
if (tp->persistent)
if (tp->persistent && (!FFlag::LuauFreezeIgnorePersistent || !ignorePersistent))
return tp;
TypeCloner cloner{NotNull{&dest}, cloneState.builtinTypes, NotNull{&cloneState.seenTypes}, NotNull{&cloneState.seenTypePacks}};
TypeCloner cloner{
NotNull{&dest},
cloneState.builtinTypes,
NotNull{&cloneState.seenTypes},
NotNull{&cloneState.seenTypePacks},
nullptr,
FFlag::LuauFreezeIgnorePersistent && ignorePersistent ? tp : nullptr
};
return cloner.shallowClone(tp);
}
TypeId shallowClone(TypeId typeId, TypeArena& dest, CloneState& cloneState)
TypeId shallowClone(TypeId typeId, TypeArena& dest, CloneState& cloneState, bool ignorePersistent)
{
if (typeId->persistent)
if (typeId->persistent && (!FFlag::LuauFreezeIgnorePersistent || !ignorePersistent))
return typeId;
TypeCloner cloner{NotNull{&dest}, cloneState.builtinTypes, NotNull{&cloneState.seenTypes}, NotNull{&cloneState.seenTypePacks}};
TypeCloner cloner{
NotNull{&dest},
cloneState.builtinTypes,
NotNull{&cloneState.seenTypes},
NotNull{&cloneState.seenTypePacks},
FFlag::LuauFreezeIgnorePersistent && ignorePersistent ? typeId : nullptr,
nullptr
};
return cloner.shallowClone(typeId);
}
@ -481,7 +510,7 @@ TypePackId clone(TypePackId tp, TypeArena& dest, CloneState& cloneState)
if (tp->persistent)
return tp;
TypeCloner cloner{NotNull{&dest}, cloneState.builtinTypes, NotNull{&cloneState.seenTypes}, NotNull{&cloneState.seenTypePacks}};
TypeCloner cloner{NotNull{&dest}, cloneState.builtinTypes, NotNull{&cloneState.seenTypes}, NotNull{&cloneState.seenTypePacks}, nullptr, nullptr};
return cloner.clone(tp);
}
@ -490,13 +519,13 @@ TypeId clone(TypeId typeId, TypeArena& dest, CloneState& cloneState)
if (typeId->persistent)
return typeId;
TypeCloner cloner{NotNull{&dest}, cloneState.builtinTypes, NotNull{&cloneState.seenTypes}, NotNull{&cloneState.seenTypePacks}};
TypeCloner cloner{NotNull{&dest}, cloneState.builtinTypes, NotNull{&cloneState.seenTypes}, NotNull{&cloneState.seenTypePacks}, nullptr, nullptr};
return cloner.clone(typeId);
}
TypeFun clone(const TypeFun& typeFun, TypeArena& dest, CloneState& cloneState)
{
TypeCloner cloner{NotNull{&dest}, cloneState.builtinTypes, NotNull{&cloneState.seenTypes}, NotNull{&cloneState.seenTypePacks}};
TypeCloner cloner{NotNull{&dest}, cloneState.builtinTypes, NotNull{&cloneState.seenTypes}, NotNull{&cloneState.seenTypePacks}, nullptr, nullptr};
TypeFun copy = typeFun;
@ -521,4 +550,18 @@ TypeFun clone(const TypeFun& typeFun, TypeArena& dest, CloneState& cloneState)
return copy;
}
Binding clone(const Binding& binding, TypeArena& dest, CloneState& cloneState)
{
TypeCloner cloner{NotNull{&dest}, cloneState.builtinTypes, NotNull{&cloneState.seenTypes}, NotNull{&cloneState.seenTypePacks}, nullptr, nullptr};
Binding b;
b.deprecated = binding.deprecated;
b.deprecatedSuggestion = binding.deprecatedSuggestion;
b.documentationSymbol = binding.documentationSymbol;
b.location = binding.location;
b.typeId = cloner.clone(binding.typeId);
return b;
}
} // namespace Luau

View file

@ -31,13 +31,14 @@
LUAU_FASTINT(LuauCheckRecursionLimit)
LUAU_FASTFLAG(DebugLuauLogSolverToJson)
LUAU_FASTFLAG(DebugLuauMagicTypes)
LUAU_FASTFLAG(LuauTypestateBuiltins2)
LUAU_FASTFLAG(LuauPreserveUnionIntersectionNodeForLeadingTokenSingleType)
LUAU_FASTFLAGVARIABLE(LuauNewSolverPrePopulateClasses)
LUAU_FASTFLAGVARIABLE(LuauNewSolverPopulateTableLocations)
LUAU_FASTFLAGVARIABLE(LuauTrackInteriorFreeTypesOnScope)
LUAU_FASTFLAGVARIABLE(InferGlobalTypes)
LUAU_FASTFLAG(LuauFreeTypesMustHaveBounds)
namespace Luau
{
@ -1087,20 +1088,10 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatLocal* stat
if (value->is<AstExprTable>())
addConstraint(scope, value->location, NameConstraint{*firstValueType, var->name.value, /*synthetic*/ true});
else if (const AstExprCall* call = value->as<AstExprCall>())
{
if (FFlag::LuauTypestateBuiltins2)
{
if (matchSetMetatable(*call))
addConstraint(scope, value->location, NameConstraint{*firstValueType, var->name.value, /*synthetic*/ true});
}
else
{
if (const AstExprGlobal* global = call->func->as<AstExprGlobal>(); global && global->name == "setmetatable")
{
addConstraint(scope, value->location, NameConstraint{*firstValueType, var->name.value, /*synthetic*/ true});
}
}
}
}
if (statLocal->values.size > 0)
@ -2068,7 +2059,7 @@ InferencePack ConstraintGenerator::checkPack(const ScopePtr& scope, AstExprCall*
return InferencePack{arena->addTypePack({resultTy}), {refinementArena.variadic(returnRefinements)}};
}
if (FFlag::LuauTypestateBuiltins2 && shouldTypestateForFirstArgument(*call) && call->args.size > 0 && isLValue(call->args.data[0]))
if (shouldTypestateForFirstArgument(*call) && call->args.size > 0 && isLValue(call->args.data[0]))
{
AstExpr* targetExpr = call->args.data[0];
auto resultTy = arena->addType(BlockedType{});
@ -2217,7 +2208,8 @@ Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprConstantStrin
if (forceSingleton)
return Inference{arena->addType(SingletonType{StringSingleton{std::string{string->value.data, string->value.size}}})};
FreeType ft = FreeType{scope.get()};
FreeType ft =
FFlag::LuauFreeTypesMustHaveBounds ? FreeType{scope.get(), builtinTypes->neverType, builtinTypes->unknownType} : FreeType{scope.get()};
ft.lowerBound = arena->addType(SingletonType{StringSingleton{std::string{string->value.data, string->value.size}}});
ft.upperBound = builtinTypes->stringType;
const TypeId freeTy = arena->addType(ft);
@ -2231,7 +2223,8 @@ Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprConstantBool*
if (forceSingleton)
return Inference{singletonType};
FreeType ft = FreeType{scope.get()};
FreeType ft =
FFlag::LuauFreeTypesMustHaveBounds ? FreeType{scope.get(), builtinTypes->neverType, builtinTypes->unknownType} : FreeType{scope.get()};
ft.lowerBound = singletonType;
ft.upperBound = builtinTypes->booleanType;
const TypeId freeTy = arena->addType(ft);
@ -3427,6 +3420,12 @@ TypeId ConstraintGenerator::resolveType(const ScopePtr& scope, AstType* ty, bool
}
else if (auto unionAnnotation = ty->as<AstTypeUnion>())
{
if (FFlag::LuauPreserveUnionIntersectionNodeForLeadingTokenSingleType)
{
if (unionAnnotation->types.size == 1)
return resolveType(scope, unionAnnotation->types.data[0], inTypeArguments);
}
std::vector<TypeId> parts;
for (AstType* part : unionAnnotation->types)
{
@ -3437,6 +3436,12 @@ TypeId ConstraintGenerator::resolveType(const ScopePtr& scope, AstType* ty, bool
}
else if (auto intersectionAnnotation = ty->as<AstTypeIntersection>())
{
if (FFlag::LuauPreserveUnionIntersectionNodeForLeadingTokenSingleType)
{
if (intersectionAnnotation->types.size == 1)
return resolveType(scope, intersectionAnnotation->types.data[0], inTypeArguments);
}
std::vector<TypeId> parts;
for (AstType* part : intersectionAnnotation->types)
{
@ -3445,6 +3450,10 @@ TypeId ConstraintGenerator::resolveType(const ScopePtr& scope, AstType* ty, bool
result = arena->addType(IntersectionType{parts});
}
else if (auto typeGroupAnnotation = ty->as<AstTypeGroup>())
{
result = resolveType(scope, typeGroupAnnotation->type, inTypeArguments);
}
else if (auto boolAnnotation = ty->as<AstTypeSingletonBool>())
{
if (boolAnnotation->value)

View file

@ -31,7 +31,6 @@ LUAU_FASTFLAGVARIABLE(DebugLuauLogSolver)
LUAU_FASTFLAGVARIABLE(DebugLuauLogSolverIncludeDependencies)
LUAU_FASTFLAGVARIABLE(DebugLuauLogBindings)
LUAU_FASTINTVARIABLE(LuauSolverRecursionLimit, 500)
LUAU_FASTFLAGVARIABLE(LuauRemoveNotAnyHack)
LUAU_FASTFLAGVARIABLE(DebugLuauEqSatSimplification)
LUAU_FASTFLAG(LuauNewSolverPopulateTableLocations)
LUAU_FASTFLAGVARIABLE(LuauAllowNilAssignmentToIndexer)
@ -1161,23 +1160,9 @@ void ConstraintSolver::fillInDiscriminantTypes(
continue;
}
if (FFlag::LuauRemoveNotAnyHack)
{
// We bind any unused discriminants to the `*no-refine*` type indicating that it can be safely ignored.
emplaceType<BoundType>(asMutable(follow(*ty)), builtinTypes->noRefineType);
}
else
{
// We use `any` here because the discriminant type may be pointed at by both branches,
// where the discriminant type is not negated, and the other where it is negated, i.e.
// `unknown ~ unknown` and `~unknown ~ never`, so `T & unknown ~ T` and `T & ~unknown ~ never`
// v.s.
// `any ~ any` and `~any ~ any`, so `T & any ~ T` and `T & ~any ~ T`
//
// In practice, users cannot negate `any`, so this is an implementation detail we can always change.
emplaceType<BoundType>(asMutable(follow(*ty)), builtinTypes->anyType);
}
}
}
bool ConstraintSolver::tryDispatch(const FunctionCallConstraint& c, NotNull<const Constraint> constraint)
@ -1313,23 +1298,9 @@ bool ConstraintSolver::tryDispatch(const FunctionCallConstraint& c, NotNull<cons
continue;
}
if (FFlag::LuauRemoveNotAnyHack)
{
// We bind any unused discriminants to the `*no-refine*` type indicating that it can be safely ignored.
emplaceType<BoundType>(asMutable(follow(*ty)), builtinTypes->noRefineType);
}
else
{
// We use `any` here because the discriminant type may be pointed at by both branches,
// where the discriminant type is not negated, and the other where it is negated, i.e.
// `unknown ~ unknown` and `~unknown ~ never`, so `T & unknown ~ T` and `T & ~unknown ~ never`
// v.s.
// `any ~ any` and `~any ~ any`, so `T & any ~ T` and `T & ~any ~ T`
//
// In practice, users cannot negate `any`, so this is an implementation detail we can always change.
emplaceType<BoundType>(asMutable(follow(*ty)), builtinTypes->anyType);
}
}
}

View file

@ -13,7 +13,6 @@
LUAU_FASTFLAG(DebugLuauFreezeArena)
LUAU_FASTFLAG(LuauSolverV2)
LUAU_FASTFLAG(LuauTypestateBuiltins2)
namespace Luau
{
@ -879,7 +878,7 @@ DataFlowResult DataFlowGraphBuilder::visitExpr(AstExprCall* c)
{
visitExpr(c->func);
if (FFlag::LuauTypestateBuiltins2 && shouldTypestateForFirstArgument(*c) && c->args.size > 1 && isLValue(*c->args.begin()))
if (shouldTypestateForFirstArgument(*c) && c->args.size > 1 && isLValue(*c->args.begin()))
{
AstExpr* firstArg = *c->args.begin();
@ -1170,6 +1169,8 @@ void DataFlowGraphBuilder::visitType(AstType* t)
return; // ok
else if (auto s = t->as<AstTypeSingletonString>())
return; // ok
else if (auto g = t->as<AstTypeGroup>())
return visitType(g->type);
else
handle->ice("Unknown AstType in DataFlowGraphBuilder::visitType");
}

View file

@ -6,6 +6,7 @@
#include "Luau/Autocomplete.h"
#include "Luau/Common.h"
#include "Luau/EqSatSimplification.h"
#include "Luau/ModuleResolver.h"
#include "Luau/Parser.h"
#include "Luau/ParseOptions.h"
#include "Luau/Module.h"
@ -19,16 +20,21 @@
#include "Luau/Parser.h"
#include "Luau/ParseOptions.h"
#include "Luau/Module.h"
#include "Luau/Clone.h"
#include "AutocompleteCore.h"
LUAU_FASTINT(LuauTypeInferRecursionLimit);
LUAU_FASTINT(LuauTypeInferIterationLimit);
LUAU_FASTINT(LuauTarjanChildLimit)
LUAU_FASTFLAG(LuauAllowFragmentParsing);
LUAU_FASTFLAG(LuauAutocompleteRefactorsForIncrementalAutocomplete)
LUAU_FASTFLAGVARIABLE(LuauIncrementalAutocompleteBugfixes)
LUAU_FASTFLAG(LuauReferenceAllocatorInNewSolver)
LUAU_FASTFLAGVARIABLE(LuauMixedModeDefFinderTraversesTypeOf)
LUAU_FASTFLAG(LuauBetterReverseDependencyTracking)
LUAU_FASTFLAGVARIABLE(LuauCloneIncrementalModule)
namespace
{
template<typename T>
@ -49,6 +55,96 @@ void copyModuleMap(Luau::DenseHashMap<K, V>& result, const Luau::DenseHashMap<K,
namespace Luau
{
template<typename K, typename V>
void cloneModuleMap(TypeArena& destArena, CloneState& cloneState, const Luau::DenseHashMap<K, V>& source, Luau::DenseHashMap<K, V>& dest)
{
for (auto [k, v] : source)
{
dest[k] = Luau::clone(v, destArena, cloneState);
}
}
struct MixedModeIncrementalTCDefFinder : public AstVisitor
{
bool visit(AstExprLocal* local) override
{
referencedLocalDefs.emplace_back(local->local, local);
return true;
}
bool visit(AstTypeTypeof* node) override
{
// We need to traverse typeof expressions because they may refer to locals that we need
// to populate the local environment for fragment typechecking. For example, `typeof(m)`
// requires that we find the local/global `m` and place it in the environment.
// The default behaviour here is to return false, and have individual visitors override
// the specific behaviour they need.
return FFlag::LuauMixedModeDefFinderTraversesTypeOf;
}
// ast defs is just a mapping from expr -> def in general
// will get built up by the dfg builder
// localDefs, we need to copy over
std::vector<std::pair<AstLocal*, AstExpr*>> referencedLocalDefs;
};
void cloneAndSquashScopes(
CloneState& cloneState,
const Scope* staleScope,
const ModulePtr& staleModule,
NotNull<TypeArena> destArena,
NotNull<DataFlowGraph> dfg,
AstStatBlock* program,
Scope* destScope
)
{
std::vector<const Scope*> scopes;
for (const Scope* current = staleScope; current; current = current->parent.get())
{
scopes.emplace_back(current);
}
// in reverse order (we need to clone the parents and override defs as we go down the list)
for (auto it = scopes.rbegin(); it != scopes.rend(); ++it)
{
const Scope* curr = *it;
// Clone the lvalue types
for (const auto& [def, ty] : curr->lvalueTypes)
destScope->lvalueTypes[def] = Luau::clone(ty, *destArena, cloneState);
// Clone the rvalueRefinements
for (const auto& [def, ty] : curr->rvalueRefinements)
destScope->rvalueRefinements[def] = Luau::clone(ty, *destArena, cloneState);
for (const auto& [n, m] : curr->importedTypeBindings)
{
std::unordered_map<Name, TypeFun> importedBindingTypes;
for (const auto& [v, tf] : m)
importedBindingTypes[v] = Luau::clone(tf, *destArena, cloneState);
destScope->importedTypeBindings[n] = m;
}
// Finally, clone up the bindings
for (const auto& [s, b] : curr->bindings)
{
destScope->bindings[s] = Luau::clone(b, *destArena, cloneState);
}
}
// The above code associates defs with TypeId's in the scope
// so that lookup to locals will succeed.
MixedModeIncrementalTCDefFinder finder;
program->visit(&finder);
std::vector<std::pair<AstLocal*, AstExpr*>> locals = std::move(finder.referencedLocalDefs);
for (auto [loc, expr] : locals)
{
if (std::optional<Binding> binding = staleScope->linearSearchForBinding(loc->name.value, true))
{
destScope->lvalueTypes[dfg->getDef(expr)] = Luau::clone(binding->typeId, *destArena, cloneState);
}
}
return;
}
static FrontendModuleResolver& getModuleResolver(Frontend& frontend, std::optional<FrontendOptions> options)
{
if (FFlag::LuauSolverV2 || !options)
@ -265,13 +361,35 @@ std::optional<FragmentParseResult> parseFragment(
return fragmentResult;
}
ModulePtr cloneModule(CloneState& cloneState, const ModulePtr& source, std::unique_ptr<Allocator> alloc)
{
freeze(source->internalTypes);
freeze(source->interfaceTypes);
ModulePtr incremental = std::make_shared<Module>();
incremental->name = source->name;
incremental->humanReadableName = source->humanReadableName;
incremental->allocator = std::move(alloc);
// Clone types
cloneModuleMap(incremental->internalTypes, cloneState, source->astTypes, incremental->astTypes);
cloneModuleMap(incremental->internalTypes, cloneState, source->astTypePacks, incremental->astTypePacks);
cloneModuleMap(incremental->internalTypes, cloneState, source->astExpectedTypes, incremental->astExpectedTypes);
cloneModuleMap(incremental->internalTypes, cloneState, source->astOverloadResolvedTypes, incremental->astOverloadResolvedTypes);
cloneModuleMap(incremental->internalTypes, cloneState, source->astForInNextTypes, incremental->astForInNextTypes);
copyModuleMap(incremental->astScopes, source->astScopes);
return incremental;
}
ModulePtr copyModule(const ModulePtr& result, std::unique_ptr<Allocator> alloc)
{
freeze(result->internalTypes);
freeze(result->interfaceTypes);
ModulePtr incrementalModule = std::make_shared<Module>();
incrementalModule->name = result->name;
incrementalModule->humanReadableName = result->humanReadableName;
incrementalModule->humanReadableName = "Incremental$" + result->humanReadableName;
incrementalModule->internalTypes.owningModule = incrementalModule.get();
incrementalModule->interfaceTypes.owningModule = incrementalModule.get();
incrementalModule->allocator = std::move(alloc);
// Don't need to keep this alive (it's already on the source module)
copyModuleVec(incrementalModule->scopes, result->scopes);
@ -290,21 +408,6 @@ ModulePtr copyModule(const ModulePtr& result, std::unique_ptr<Allocator> alloc)
return incrementalModule;
}
struct MixedModeIncrementalTCDefFinder : public AstVisitor
{
bool visit(AstExprLocal* local) override
{
referencedLocalDefs.push_back({local->local, local});
return true;
}
// ast defs is just a mapping from expr -> def in general
// will get built up by the dfg builder
// localDefs, we need to copy over
std::vector<std::pair<AstLocal*, AstExpr*>> referencedLocalDefs;
};
void mixedModeCompatibility(
const ScopePtr& bottomScopeStale,
const ScopePtr& myFakeScope,
@ -343,7 +446,9 @@ FragmentTypeCheckResult typecheckFragment_(
{
freeze(stale->internalTypes);
freeze(stale->interfaceTypes);
ModulePtr incrementalModule = copyModule(stale, std::move(astAllocator));
CloneState cloneState{frontend.builtinTypes};
ModulePtr incrementalModule =
FFlag::LuauCloneIncrementalModule ? cloneModule(cloneState, stale, std::move(astAllocator)) : copyModule(stale, std::move(astAllocator));
incrementalModule->checkedInNewSolver = true;
unfreeze(incrementalModule->internalTypes);
unfreeze(incrementalModule->interfaceTypes);
@ -391,25 +496,34 @@ FragmentTypeCheckResult typecheckFragment_(
NotNull{&dfg},
{}
};
cg.rootScope = stale->getModuleScope().get();
// Any additions to the scope must occur in a fresh scope
auto freshChildOfNearestScope = std::make_shared<Scope>(closestScope);
std::shared_ptr<Scope> freshChildOfNearestScope = nullptr;
if (FFlag::LuauCloneIncrementalModule)
{
freshChildOfNearestScope = std::make_shared<Scope>(closestScope);
incrementalModule->scopes.emplace_back(root->location, freshChildOfNearestScope);
cg.rootScope = freshChildOfNearestScope.get();
// Update freshChildOfNearestScope with the appropriate lvalueTypes
cloneAndSquashScopes(
cloneState, closestScope.get(), stale, NotNull{&incrementalModule->internalTypes}, NotNull{&dfg}, root, freshChildOfNearestScope.get()
);
cg.visitFragmentRoot(freshChildOfNearestScope, root);
}
else
{
// Any additions to the scope must occur in a fresh scope
cg.rootScope = stale->getModuleScope().get();
freshChildOfNearestScope = std::make_shared<Scope>(closestScope);
incrementalModule->scopes.emplace_back(root->location, freshChildOfNearestScope);
mixedModeCompatibility(closestScope, freshChildOfNearestScope, stale, NotNull{&dfg}, root);
// closest Scope -> children = { ...., freshChildOfNearestScope}
// We need to trim nearestChild from the scope hierarcy
closestScope->children.push_back(NotNull{freshChildOfNearestScope.get()});
// Visit just the root - we know the scope it should be in
closestScope->children.emplace_back(freshChildOfNearestScope.get());
cg.visitFragmentRoot(freshChildOfNearestScope, root);
// Trim nearestChild from the closestScope
Scope* back = closestScope->children.back().get();
LUAU_ASSERT(back == freshChildOfNearestScope.get());
closestScope->children.pop_back();
}
/// Initialize the constraint solver and run it
ConstraintSolver cs{
@ -458,6 +572,13 @@ std::pair<FragmentTypeCheckStatus, FragmentTypeCheckResult> typecheckFragment(
std::optional<Position> fragmentEndPosition
)
{
if (FFlag::LuauBetterReverseDependencyTracking)
{
if (!frontend.allModuleDependenciesValid(moduleName, opts && opts->forAutocomplete))
return {FragmentTypeCheckStatus::SkipAutocomplete, {}};
}
const SourceModule* sourceModule = frontend.getSourceModule(moduleName);
if (!sourceModule)
{
@ -473,6 +594,14 @@ std::pair<FragmentTypeCheckStatus, FragmentTypeCheckResult> typecheckFragment(
return {};
}
if (FFlag::LuauIncrementalAutocompleteBugfixes && FFlag::LuauReferenceAllocatorInNewSolver)
{
if (sourceModule->allocator.get() != module->allocator.get())
{
return {FragmentTypeCheckStatus::SkipAutocomplete, {}};
}
}
auto tryParse = parseFragment(*sourceModule, src, cursorPos, fragmentEndPosition);
if (!tryParse)

View file

@ -47,6 +47,8 @@ LUAU_FASTFLAGVARIABLE(DebugLuauForceStrictMode)
LUAU_FASTFLAGVARIABLE(DebugLuauForceNonStrictMode)
LUAU_DYNAMIC_FASTFLAGVARIABLE(LuauRunCustomModuleChecks, false)
LUAU_FASTFLAGVARIABLE(LuauBetterReverseDependencyTracking)
LUAU_FASTFLAG(StudioReportLuauAny2)
LUAU_FASTFLAGVARIABLE(LuauStoreSolverTypeOnModule)
@ -820,6 +822,16 @@ bool Frontend::parseGraph(
topseen = Permanent;
buildQueue.push_back(top->name);
if (FFlag::LuauBetterReverseDependencyTracking)
{
// at this point we know all valid dependencies are processed into SourceNodes
for (const ModuleName& dep : top->requireSet)
{
if (auto it = sourceNodes.find(dep); it != sourceNodes.end())
it->second->dependents.insert(top->name);
}
}
}
else
{
@ -1107,6 +1119,39 @@ void Frontend::recordItemResult(const BuildQueueItem& item)
if (item.exception)
std::rethrow_exception(item.exception);
if (FFlag::LuauBetterReverseDependencyTracking)
{
bool replacedModule = false;
if (item.options.forAutocomplete)
{
replacedModule = moduleResolverForAutocomplete.setModule(item.name, item.module);
item.sourceNode->dirtyModuleForAutocomplete = false;
}
else
{
replacedModule = moduleResolver.setModule(item.name, item.module);
item.sourceNode->dirtyModule = false;
}
if (replacedModule)
{
LUAU_TIMETRACE_SCOPE("Frontend::invalidateDependentModules", "Frontend");
LUAU_TIMETRACE_ARGUMENT("name", item.name.c_str());
traverseDependents(
item.name,
[forAutocomplete = item.options.forAutocomplete](SourceNode& sourceNode)
{
bool traverseSubtree = !sourceNode.hasInvalidModuleDependency(forAutocomplete);
sourceNode.setInvalidModuleDependency(true, forAutocomplete);
return traverseSubtree;
}
);
}
item.sourceNode->setInvalidModuleDependency(false, item.options.forAutocomplete);
}
else
{
if (item.options.forAutocomplete)
{
moduleResolverForAutocomplete.setModule(item.name, item.module);
@ -1117,6 +1162,7 @@ void Frontend::recordItemResult(const BuildQueueItem& item)
moduleResolver.setModule(item.name, item.module);
item.sourceNode->dirtyModule = false;
}
}
stats.timeCheck += item.stats.timeCheck;
stats.timeLint += item.stats.timeLint;
@ -1152,6 +1198,13 @@ ScopePtr Frontend::getModuleEnvironment(const SourceModule& module, const Config
return result;
}
bool Frontend::allModuleDependenciesValid(const ModuleName& name, bool forAutocomplete) const
{
LUAU_ASSERT(FFlag::LuauBetterReverseDependencyTracking);
auto it = sourceNodes.find(name);
return it != sourceNodes.end() && !it->second->hasInvalidModuleDependency(forAutocomplete);
}
bool Frontend::isDirty(const ModuleName& name, bool forAutocomplete) const
{
auto it = sourceNodes.find(name);
@ -1165,6 +1218,31 @@ bool Frontend::isDirty(const ModuleName& name, bool forAutocomplete) const
* It would be nice for this function to be O(1)
*/
void Frontend::markDirty(const ModuleName& name, std::vector<ModuleName>* markedDirty)
{
LUAU_TIMETRACE_SCOPE("Frontend::markDirty", "Frontend");
LUAU_TIMETRACE_ARGUMENT("name", name.c_str());
if (FFlag::LuauBetterReverseDependencyTracking)
{
traverseDependents(
name,
[markedDirty](SourceNode& sourceNode)
{
if (markedDirty)
markedDirty->push_back(sourceNode.name);
if (sourceNode.dirtySourceModule && sourceNode.dirtyModule && sourceNode.dirtyModuleForAutocomplete)
return false;
sourceNode.dirtySourceModule = true;
sourceNode.dirtyModule = true;
sourceNode.dirtyModuleForAutocomplete = true;
return true;
}
);
}
else
{
if (sourceNodes.count(name) == 0)
return;
@ -1205,6 +1283,33 @@ void Frontend::markDirty(const ModuleName& name, std::vector<ModuleName>* marked
queue.insert(queue.end(), dependents.begin(), dependents.end());
}
}
}
void Frontend::traverseDependents(const ModuleName& name, std::function<bool(SourceNode&)> processSubtree)
{
LUAU_ASSERT(FFlag::LuauBetterReverseDependencyTracking);
LUAU_TIMETRACE_SCOPE("Frontend::traverseDependents", "Frontend");
if (sourceNodes.count(name) == 0)
return;
std::vector<ModuleName> queue{name};
while (!queue.empty())
{
ModuleName next = std::move(queue.back());
queue.pop_back();
LUAU_ASSERT(sourceNodes.count(next) > 0);
SourceNode& sourceNode = *sourceNodes[next];
if (!processSubtree(sourceNode))
continue;
const Set<ModuleName>& dependents = sourceNode.dependents;
queue.insert(queue.end(), dependents.begin(), dependents.end());
}
}
SourceModule* Frontend::getSourceModule(const ModuleName& moduleName)
{
@ -1643,6 +1748,17 @@ std::pair<SourceNode*, SourceModule*> Frontend::getSourceNode(const ModuleName&
sourceNode->name = sourceModule->name;
sourceNode->humanReadableName = sourceModule->humanReadableName;
if (FFlag::LuauBetterReverseDependencyTracking)
{
// clear all prior dependents. we will re-add them after parsing the rest of the graph
for (const auto& [moduleName, _] : sourceNode->requireLocations)
{
if (auto depIt = sourceNodes.find(moduleName); depIt != sourceNodes.end())
depIt->second->dependents.erase(sourceNode->name);
}
}
sourceNode->requireSet.clear();
sourceNode->requireLocations.clear();
sourceNode->dirtySourceModule = false;
@ -1764,11 +1880,21 @@ std::string FrontendModuleResolver::getHumanReadableModuleName(const ModuleName&
return frontend->fileResolver->getHumanReadableModuleName(moduleName);
}
void FrontendModuleResolver::setModule(const ModuleName& moduleName, ModulePtr module)
bool FrontendModuleResolver::setModule(const ModuleName& moduleName, ModulePtr module)
{
std::scoped_lock lock(moduleMutex);
if (FFlag::LuauBetterReverseDependencyTracking)
{
bool replaced = modules.count(moduleName) > 0;
modules[moduleName] = std::move(module);
return replaced;
}
else
{
modules[moduleName] = std::move(module);
return false;
}
}
void FrontendModuleResolver::clearModules()

View file

@ -11,6 +11,7 @@
#include <algorithm>
LUAU_FASTFLAG(LuauSolverV2)
LUAU_FASTFLAG(LuauFreeTypesMustHaveBounds)
namespace Luau
{
@ -163,7 +164,7 @@ TypeId ReplaceGenerics::clean(TypeId ty)
}
else
{
return addType(FreeType{scope, level});
return FFlag::LuauFreeTypesMustHaveBounds ? arena->freshType(builtinTypes, scope, level) : addType(FreeType{scope, level});
}
}

View file

@ -20,6 +20,7 @@
#include <iterator>
LUAU_FASTFLAGVARIABLE(LuauCountSelfCallsNonstrict)
LUAU_FASTFLAG(LuauFreeTypesMustHaveBounds)
namespace Luau
{
@ -211,7 +212,7 @@ struct NonStrictTypeChecker
return *fst;
else if (auto ftp = get<FreeTypePack>(pack))
{
TypeId result = arena->addType(FreeType{ftp->scope});
TypeId result = FFlag::LuauFreeTypesMustHaveBounds ? arena->freshType(builtinTypes, ftp->scope) : arena->addType(FreeType{ftp->scope});
TypePackId freeTail = arena->addTypePack(FreeTypePack{ftp->scope});
TypePack* resultPack = emplaceTypePack<TypePack>(asMutable(pack));

View file

@ -18,10 +18,10 @@
LUAU_FASTFLAGVARIABLE(DebugLuauCheckNormalizeInvariant)
LUAU_FASTINTVARIABLE(LuauNormalizeCacheLimit, 100000)
LUAU_FASTFLAG(LuauSolverV2)
LUAU_FASTINTVARIABLE(LuauNormalizeIntersectionLimit, 200)
LUAU_FASTFLAGVARIABLE(LuauNormalizationTracksCyclicPairsThroughInhabitance)
LUAU_FASTFLAG(LuauSolverV2)
LUAU_FASTFLAGVARIABLE(LuauFixInfiniteRecursionInNormalization)
LUAU_FASTFLAGVARIABLE(LuauFixNormalizedIntersectionOfNegatedClass)
namespace Luau
{
@ -2284,9 +2284,24 @@ void Normalizer::intersectClassesWithClass(NormalizedClassType& heres, TypeId th
else if (isSubclass(there, hereTy))
{
TypeIds negations = std::move(hereNegations);
bool emptyIntersectWithNegation = false;
for (auto nIt = negations.begin(); nIt != negations.end();)
{
if (FFlag::LuauFixNormalizedIntersectionOfNegatedClass && isSubclass(there, *nIt))
{
// Hitting this block means that the incoming class is a
// subclass of this type, _and_ one of its negations is a
// superclass of this type, e.g.:
//
// Dog & ~Animal
//
// Clearly this intersects to never, so we mark this class as
// being removed from the normalized class type.
emptyIntersectWithNegation = true;
break;
}
if (!isSubclass(*nIt, there))
{
nIt = negations.erase(nIt);
@ -2299,6 +2314,7 @@ void Normalizer::intersectClassesWithClass(NormalizedClassType& heres, TypeId th
it = heres.ordering.erase(it);
heres.classes.erase(hereTy);
if (!emptyIntersectWithNegation)
heres.pushPair(there, std::move(negations));
break;
}
@ -2583,12 +2599,32 @@ std::optional<TypeId> Normalizer::intersectionOfTables(TypeId here, TypeId there
if (hprop.readTy.has_value())
{
if (tprop.readTy.has_value())
{
if (FFlag::LuauFixInfiniteRecursionInNormalization)
{
TypeId ty = simplifyIntersection(builtinTypes, NotNull{arena}, *hprop.readTy, *tprop.readTy).result;
// If any property is going to get mapped to `never`, we can just call the entire table `never`.
// Since this check is syntactic, we may sometimes miss simplifying tables with complex uninhabited properties.
// Prior versions of this code attempted to do this semantically using the normalization machinery, but this
// mistakenly causes infinite loops when giving more complex recursive table types. As it stands, this approach
// will continue to scale as simplification is improved, but we may wish to reintroduce the semantic approach
// once we have revisited the usage of seen sets systematically (and possibly with some additional guarding to recognize
// when types are infinitely-recursive with non-pointer identical instances of them, or some guard to prevent that
// construction altogether). See also: `gh1632_no_infinite_recursion_in_normalization`
if (get<NeverType>(ty))
return {builtinTypes->neverType};
prop.readTy = ty;
hereSubThere &= (ty == hprop.readTy);
thereSubHere &= (ty == tprop.readTy);
}
else
{
// if the intersection of the read types of a property is uninhabited, the whole table is `never`.
// We've seen these table prop elements before and we're about to ask if their intersection
// is inhabited
if (FFlag::LuauNormalizationTracksCyclicPairsThroughInhabitance)
{
auto pair1 = std::pair{*hprop.readTy, *tprop.readTy};
auto pair2 = std::pair{*tprop.readTy, *hprop.readTy};
if (seenTablePropPairs.contains(pair1) || seenTablePropPairs.contains(pair2))
@ -2603,6 +2639,8 @@ std::optional<TypeId> Normalizer::intersectionOfTables(TypeId here, TypeId there
seenTablePropPairs.insert(pair2);
}
// FIXME(ariel): this is being added in a flag removal, so not changing the semantics here, but worth noting that this
// fresh `seenSet` is definitely a bug. we already have `seenSet` from the parameter that _should_ have been used here.
Set<TypeId> seenSet{nullptr};
NormalizationResult res = isIntersectionInhabited(*hprop.readTy, *tprop.readTy, seenTablePropPairs, seenSet);
@ -2616,34 +2654,6 @@ std::optional<TypeId> Normalizer::intersectionOfTables(TypeId here, TypeId there
hereSubThere &= (ty == hprop.readTy);
thereSubHere &= (ty == tprop.readTy);
}
else
{
if (seenSet.contains(*hprop.readTy) && seenSet.contains(*tprop.readTy))
{
seenSet.erase(*hprop.readTy);
seenSet.erase(*tprop.readTy);
return {builtinTypes->neverType};
}
else
{
seenSet.insert(*hprop.readTy);
seenSet.insert(*tprop.readTy);
}
NormalizationResult res = isIntersectionInhabited(*hprop.readTy, *tprop.readTy);
seenSet.erase(*hprop.readTy);
seenSet.erase(*tprop.readTy);
if (NormalizationResult::True != res)
return {builtinTypes->neverType};
TypeId ty = simplifyIntersection(builtinTypes, NotNull{arena}, *hprop.readTy, *tprop.readTy).result;
prop.readTy = ty;
hereSubThere &= (ty == hprop.readTy);
thereSubHere &= (ty == tprop.readTy);
}
}
else
{

View file

@ -31,16 +31,16 @@ struct TypeSimplifier
int recursionDepth = 0;
TypeId mkNegation(TypeId ty);
TypeId mkNegation(TypeId ty) const;
TypeId intersectFromParts(std::set<TypeId> parts);
TypeId intersectUnionWithType(TypeId unionTy, TypeId right);
TypeId intersectUnionWithType(TypeId left, TypeId right);
TypeId intersectUnions(TypeId left, TypeId right);
TypeId intersectNegatedUnion(TypeId unionTy, TypeId right);
TypeId intersectNegatedUnion(TypeId left, TypeId right);
TypeId intersectTypeWithNegation(TypeId a, TypeId b);
TypeId intersectNegations(TypeId a, TypeId b);
TypeId intersectTypeWithNegation(TypeId left, TypeId right);
TypeId intersectNegations(TypeId left, TypeId right);
TypeId intersectIntersectionWithType(TypeId left, TypeId right);
@ -48,8 +48,8 @@ struct TypeSimplifier
// unions, intersections, or negations.
std::optional<TypeId> basicIntersect(TypeId left, TypeId right);
TypeId intersect(TypeId ty, TypeId discriminant);
TypeId union_(TypeId ty, TypeId discriminant);
TypeId intersect(TypeId left, TypeId right);
TypeId union_(TypeId left, TypeId right);
TypeId simplify(TypeId ty);
TypeId simplify(TypeId ty, DenseHashSet<TypeId>& seen);
@ -573,7 +573,7 @@ Relation relate(TypeId left, TypeId right)
return relate(left, right, seen);
}
TypeId TypeSimplifier::mkNegation(TypeId ty)
TypeId TypeSimplifier::mkNegation(TypeId ty) const
{
TypeId result = nullptr;

View file

@ -22,7 +22,6 @@
#include <algorithm>
LUAU_FASTFLAGVARIABLE(DebugLuauSubtypingCheckPathValidity)
LUAU_FASTFLAGVARIABLE(LuauRetrySubtypingWithoutHiddenPack)
namespace Luau
{
@ -1474,7 +1473,7 @@ SubtypingResult Subtyping::isCovariantWith(
// If subtyping failed in the argument packs, we should check if there's a hidden variadic tail and try ignoring it.
// This might cause subtyping correctly because the sub type here may not have a hidden variadic tail or equivalent.
if (FFlag::LuauRetrySubtypingWithoutHiddenPack && !result.isSubtype)
if (!result.isSubtype)
{
auto [arguments, tail] = flatten(superFunction->argTypes);

File diff suppressed because it is too large Load diff

View file

@ -27,6 +27,7 @@ LUAU_FASTINTVARIABLE(LuauTypeMaximumStringifierLength, 500)
LUAU_FASTINTVARIABLE(LuauTableTypeMaximumStringifierLength, 0)
LUAU_FASTINT(LuauTypeInferRecursionLimit)
LUAU_FASTFLAG(LuauInstantiateInSubtyping)
LUAU_FASTFLAGVARIABLE(LuauFreeTypesMustHaveBounds)
namespace Luau
{
@ -478,24 +479,12 @@ bool hasLength(TypeId ty, DenseHashSet<TypeId>& seen, int* recursionCount)
return false;
}
FreeType::FreeType(TypeLevel level)
// New constructors
FreeType::FreeType(TypeLevel level, TypeId lowerBound, TypeId upperBound)
: index(Unifiable::freshIndex())
, level(level)
, scope(nullptr)
{
}
FreeType::FreeType(Scope* scope)
: index(Unifiable::freshIndex())
, level{}
, scope(scope)
{
}
FreeType::FreeType(Scope* scope, TypeLevel level)
: index(Unifiable::freshIndex())
, level(level)
, scope(scope)
, lowerBound(lowerBound)
, upperBound(upperBound)
{
}
@ -507,6 +496,40 @@ FreeType::FreeType(Scope* scope, TypeId lowerBound, TypeId upperBound)
{
}
FreeType::FreeType(Scope* scope, TypeLevel level, TypeId lowerBound, TypeId upperBound)
: index(Unifiable::freshIndex())
, level(level)
, scope(scope)
, lowerBound(lowerBound)
, upperBound(upperBound)
{
}
// Old constructors
FreeType::FreeType(TypeLevel level)
: index(Unifiable::freshIndex())
, level(level)
, scope(nullptr)
{
LUAU_ASSERT(!FFlag::LuauFreeTypesMustHaveBounds);
}
FreeType::FreeType(Scope* scope)
: index(Unifiable::freshIndex())
, level{}
, scope(scope)
{
LUAU_ASSERT(!FFlag::LuauFreeTypesMustHaveBounds);
}
FreeType::FreeType(Scope* scope, TypeLevel level)
: index(Unifiable::freshIndex())
, level(level)
, scope(scope)
{
LUAU_ASSERT(!FFlag::LuauFreeTypesMustHaveBounds);
}
GenericType::GenericType()
: index(Unifiable::freshIndex())
, name("g" + std::to_string(index))

View file

@ -3,6 +3,7 @@
#include "Luau/TypeArena.h"
LUAU_FASTFLAGVARIABLE(DebugLuauFreezeArena);
LUAU_FASTFLAG(LuauFreeTypesMustHaveBounds)
namespace Luau
{
@ -22,7 +23,34 @@ TypeId TypeArena::addTV(Type&& tv)
return allocated;
}
TypeId TypeArena::freshType(TypeLevel level)
TypeId TypeArena::freshType(NotNull<BuiltinTypes> builtins, TypeLevel level)
{
TypeId allocated = types.allocate(FreeType{level, builtins->neverType, builtins->unknownType});
asMutable(allocated)->owningArena = this;
return allocated;
}
TypeId TypeArena::freshType(NotNull<BuiltinTypes> builtins, Scope* scope)
{
TypeId allocated = types.allocate(FreeType{scope, builtins->neverType, builtins->unknownType});
asMutable(allocated)->owningArena = this;
return allocated;
}
TypeId TypeArena::freshType(NotNull<BuiltinTypes> builtins, Scope* scope, TypeLevel level)
{
TypeId allocated = types.allocate(FreeType{scope, level, builtins->neverType, builtins->unknownType});
asMutable(allocated)->owningArena = this;
return allocated;
}
TypeId TypeArena::freshType_DEPRECATED(TypeLevel level)
{
TypeId allocated = types.allocate(FreeType{level});
@ -31,7 +59,7 @@ TypeId TypeArena::freshType(TypeLevel level)
return allocated;
}
TypeId TypeArena::freshType(Scope* scope)
TypeId TypeArena::freshType_DEPRECATED(Scope* scope)
{
TypeId allocated = types.allocate(FreeType{scope});
@ -40,7 +68,7 @@ TypeId TypeArena::freshType(Scope* scope)
return allocated;
}
TypeId TypeArena::freshType(Scope* scope, TypeLevel level)
TypeId TypeArena::freshType_DEPRECATED(Scope* scope, TypeLevel level)
{
TypeId allocated = types.allocate(FreeType{scope, level});

View file

@ -31,6 +31,7 @@ LUAU_FASTFLAG(DebugLuauMagicTypes)
LUAU_FASTFLAG(InferGlobalTypes)
LUAU_FASTFLAGVARIABLE(LuauTableKeysAreRValues)
LUAU_FASTFLAG(LuauFreeTypesMustHaveBounds)
namespace Luau
{
@ -2105,7 +2106,10 @@ TypeId TypeChecker2::visit(AstExprBinary* expr, AstNode* overrideKey)
}
else
{
expectedRets = module->internalTypes.addTypePack({module->internalTypes.freshType(scope, TypeLevel{})});
expectedRets = module->internalTypes.addTypePack(
{FFlag::LuauFreeTypesMustHaveBounds ? module->internalTypes.freshType(builtinTypes, scope, TypeLevel{})
: module->internalTypes.freshType_DEPRECATED(scope, TypeLevel{})}
);
}
TypeId expectedTy = module->internalTypes.addType(FunctionType(expectedArgs, expectedRets));
@ -2357,7 +2361,8 @@ TypeId TypeChecker2::flattenPack(TypePackId pack)
return *fst;
else if (auto ftp = get<FreeTypePack>(pack))
{
TypeId result = module->internalTypes.addType(FreeType{ftp->scope});
TypeId result = FFlag::LuauFreeTypesMustHaveBounds ? module->internalTypes.freshType(builtinTypes, ftp->scope)
: module->internalTypes.addType(FreeType{ftp->scope});
TypePackId freeTail = module->internalTypes.addTypePack(FreeTypePack{ftp->scope});
TypePack* resultPack = emplaceTypePack<TypePack>(asMutable(pack));
@ -2419,6 +2424,8 @@ void TypeChecker2::visit(AstType* ty)
return visit(t);
else if (auto t = ty->as<AstTypeIntersection>())
return visit(t);
else if (auto t = ty->as<AstTypeGroup>())
return visit(t->type);
}
void TypeChecker2::visit(AstTypeReference* ty)

View file

@ -47,7 +47,9 @@ LUAU_DYNAMIC_FASTINTVARIABLE(LuauTypeFamilyUseGuesserDepth, -1);
LUAU_FASTFLAGVARIABLE(DebugLuauLogTypeFamilies)
LUAU_FASTFLAG(DebugLuauEqSatSimplification)
LUAU_FASTFLAG(LuauRemoveNotAnyHack)
LUAU_FASTFLAGVARIABLE(LuauMetatableTypeFunctions)
LUAU_FASTFLAGVARIABLE(LuauClipNestedAndRecursiveUnion)
LUAU_FASTFLAGVARIABLE(LuauDoNotGeneralizeInTypeFunctions)
namespace Luau
{
@ -825,7 +827,7 @@ TypeFunctionReductionResult<TypeId> lenTypeFunction(
return {std::nullopt, Reduction::MaybeOk, {operandTy}, {}};
// if the type is free but has only one remaining reference, we can generalize it to its upper bound here.
if (ctx->solver)
if (ctx->solver && !FFlag::LuauDoNotGeneralizeInTypeFunctions)
{
std::optional<TypeId> maybeGeneralized = ctx->solver->generalizeFreeType(ctx->scope, operandTy, /* avoidSealingTables */ true);
if (!maybeGeneralized)
@ -917,7 +919,7 @@ TypeFunctionReductionResult<TypeId> unmTypeFunction(
return {std::nullopt, Reduction::MaybeOk, {operandTy}, {}};
// if the type is free but has only one remaining reference, we can generalize it to its upper bound here.
if (ctx->solver)
if (ctx->solver && !FFlag::LuauDoNotGeneralizeInTypeFunctions)
{
std::optional<TypeId> maybeGeneralized = ctx->solver->generalizeFreeType(ctx->scope, operandTy);
if (!maybeGeneralized)
@ -1030,7 +1032,7 @@ std::optional<std::string> TypeFunctionRuntime::registerFunction(AstStatTypeFunc
AstStat* stmtArray[] = {&stmtReturn};
AstArray<AstStat*> stmts{stmtArray, 1};
AstStatBlock exec{Location{}, stmts};
ParseResult parseResult{&exec, 1};
ParseResult parseResult{&exec, 1, {}, {}, {}, CstNodeMap{nullptr}};
BytecodeBuilder builder;
try
@ -1160,7 +1162,7 @@ TypeFunctionReductionResult<TypeId> numericBinopTypeFunction(
return {std::nullopt, Reduction::MaybeOk, {rhsTy}, {}};
// if either type is free but has only one remaining reference, we can generalize it to its upper bound here.
if (ctx->solver)
if (ctx->solver && !FFlag::LuauDoNotGeneralizeInTypeFunctions)
{
std::optional<TypeId> lhsMaybeGeneralized = ctx->solver->generalizeFreeType(ctx->scope, lhsTy);
std::optional<TypeId> rhsMaybeGeneralized = ctx->solver->generalizeFreeType(ctx->scope, rhsTy);
@ -1397,7 +1399,7 @@ TypeFunctionReductionResult<TypeId> concatTypeFunction(
return {std::nullopt, Reduction::MaybeOk, {rhsTy}, {}};
// if either type is free but has only one remaining reference, we can generalize it to its upper bound here.
if (ctx->solver)
if (ctx->solver && !FFlag::LuauDoNotGeneralizeInTypeFunctions)
{
std::optional<TypeId> lhsMaybeGeneralized = ctx->solver->generalizeFreeType(ctx->scope, lhsTy);
std::optional<TypeId> rhsMaybeGeneralized = ctx->solver->generalizeFreeType(ctx->scope, rhsTy);
@ -1512,7 +1514,7 @@ TypeFunctionReductionResult<TypeId> andTypeFunction(
return {std::nullopt, Reduction::MaybeOk, {rhsTy}, {}};
// if either type is free but has only one remaining reference, we can generalize it to its upper bound here.
if (ctx->solver)
if (ctx->solver && !FFlag::LuauDoNotGeneralizeInTypeFunctions)
{
std::optional<TypeId> lhsMaybeGeneralized = ctx->solver->generalizeFreeType(ctx->scope, lhsTy);
std::optional<TypeId> rhsMaybeGeneralized = ctx->solver->generalizeFreeType(ctx->scope, rhsTy);
@ -1567,7 +1569,7 @@ TypeFunctionReductionResult<TypeId> orTypeFunction(
return {std::nullopt, Reduction::MaybeOk, {rhsTy}, {}};
// if either type is free but has only one remaining reference, we can generalize it to its upper bound here.
if (ctx->solver)
if (ctx->solver && !FFlag::LuauDoNotGeneralizeInTypeFunctions)
{
std::optional<TypeId> lhsMaybeGeneralized = ctx->solver->generalizeFreeType(ctx->scope, lhsTy);
std::optional<TypeId> rhsMaybeGeneralized = ctx->solver->generalizeFreeType(ctx->scope, rhsTy);
@ -1653,7 +1655,7 @@ static TypeFunctionReductionResult<TypeId> comparisonTypeFunction(
rhsTy = follow(rhsTy);
// if either type is free but has only one remaining reference, we can generalize it to its upper bound here.
if (ctx->solver)
if (ctx->solver && !FFlag::LuauDoNotGeneralizeInTypeFunctions)
{
std::optional<TypeId> lhsMaybeGeneralized = ctx->solver->generalizeFreeType(ctx->scope, lhsTy);
std::optional<TypeId> rhsMaybeGeneralized = ctx->solver->generalizeFreeType(ctx->scope, rhsTy);
@ -1791,7 +1793,7 @@ TypeFunctionReductionResult<TypeId> eqTypeFunction(
return {std::nullopt, Reduction::MaybeOk, {rhsTy}, {}};
// if either type is free but has only one remaining reference, we can generalize it to its upper bound here.
if (ctx->solver)
if (ctx->solver && !FFlag::LuauDoNotGeneralizeInTypeFunctions)
{
std::optional<TypeId> lhsMaybeGeneralized = ctx->solver->generalizeFreeType(ctx->scope, lhsTy);
std::optional<TypeId> rhsMaybeGeneralized = ctx->solver->generalizeFreeType(ctx->scope, rhsTy);
@ -1936,7 +1938,7 @@ TypeFunctionReductionResult<TypeId> refineTypeFunction(
auto stepRefine = [&ctx](TypeId target, TypeId discriminant) -> std::pair<TypeId, std::vector<TypeId>>
{
std::vector<TypeId> toBlock;
if (ctx->solver)
if (ctx->solver && !FFlag::LuauDoNotGeneralizeInTypeFunctions)
{
std::optional<TypeId> targetMaybeGeneralized = ctx->solver->generalizeFreeType(ctx->scope, target);
std::optional<TypeId> discriminantMaybeGeneralized = ctx->solver->generalizeFreeType(ctx->scope, discriminant);
@ -1987,18 +1989,10 @@ TypeFunctionReductionResult<TypeId> refineTypeFunction(
* We need to treat T & ~any as T in this case.
*/
if (auto nt = get<NegationType>(discriminant))
{
if (FFlag::LuauRemoveNotAnyHack)
{
if (get<NoRefineType>(follow(nt->ty)))
return {target, {}};
}
else
{
if (get<AnyType>(follow(nt->ty)))
return {target, {}};
}
}
// If the target type is a table, then simplification already implements the logic to deal with refinements properly since the
// type of the discriminant is guaranteed to only ever be an (arbitrarily-nested) table of a single property type.
@ -2070,7 +2064,7 @@ TypeFunctionReductionResult<TypeId> singletonTypeFunction(
return {std::nullopt, Reduction::MaybeOk, {type}, {}};
// if the type is free but has only one remaining reference, we can generalize it to its upper bound here.
if (ctx->solver)
if (ctx->solver && !FFlag::LuauDoNotGeneralizeInTypeFunctions)
{
std::optional<TypeId> maybeGeneralized = ctx->solver->generalizeFreeType(ctx->scope, type);
if (!maybeGeneralized)
@ -2091,6 +2085,43 @@ TypeFunctionReductionResult<TypeId> singletonTypeFunction(
return {ctx->builtins->unknownType, Reduction::MaybeOk, {}, {}};
}
struct CollectUnionTypeOptions : TypeOnceVisitor
{
NotNull<TypeFunctionContext> ctx;
DenseHashSet<TypeId> options{nullptr};
DenseHashSet<TypeId> blockingTypes{nullptr};
explicit CollectUnionTypeOptions(NotNull<TypeFunctionContext> ctx)
: TypeOnceVisitor(/* skipBoundTypes */ true)
, ctx(ctx)
{
}
bool visit(TypeId ty) override
{
options.insert(ty);
if (isPending(ty, ctx->solver))
blockingTypes.insert(ty);
return false;
}
bool visit(TypePackId tp) override
{
return false;
}
bool visit(TypeId ty, const TypeFunctionInstanceType& tfit) override
{
if (tfit.function->name != builtinTypeFunctions().unionFunc.name)
{
options.insert(ty);
blockingTypes.insert(ty);
return false;
}
return true;
}
};
TypeFunctionReductionResult<TypeId> unionTypeFunction(
TypeId instance,
const std::vector<TypeId>& typeParams,
@ -2108,6 +2139,35 @@ TypeFunctionReductionResult<TypeId> unionTypeFunction(
if (typeParams.size() == 1)
return {follow(typeParams[0]), Reduction::MaybeOk, {}, {}};
if (FFlag::LuauClipNestedAndRecursiveUnion)
{
CollectUnionTypeOptions collector{ctx};
collector.traverse(instance);
if (!collector.blockingTypes.empty())
{
std::vector<TypeId> blockingTypes{collector.blockingTypes.begin(), collector.blockingTypes.end()};
return {std::nullopt, Reduction::MaybeOk, std::move(blockingTypes), {}};
}
TypeId resultTy = ctx->builtins->neverType;
for (auto ty : collector.options)
{
SimplifyResult result = simplifyUnion(ctx->builtins, ctx->arena, resultTy, ty);
// This condition might fire if one of the arguments to this type
// function is a free type somewhere deep in a nested union or
// intersection type, even though we ran a pass above to capture
// some blocked types.
if (!result.blockedTypes.empty())
return {std::nullopt, Reduction::MaybeOk, {result.blockedTypes.begin(), result.blockedTypes.end()}, {}};
resultTy = result.result;
}
return {resultTy, Reduction::MaybeOk, {}, {}};
}
// we need to follow all of the type parameters.
std::vector<TypeId> types;
types.reserve(typeParams.size());
@ -2179,14 +2239,11 @@ TypeFunctionReductionResult<TypeId> intersectTypeFunction(
for (auto ty : typeParams)
types.emplace_back(follow(ty));
if (FFlag::LuauRemoveNotAnyHack)
{
// if we only have two parameters and one is `*no-refine*`, we're all done.
if (types.size() == 2 && get<NoRefineType>(types[1]))
return {types[0], Reduction::MaybeOk, {}, {}};
else if (types.size() == 2 && get<NoRefineType>(types[0]))
return {types[1], Reduction::MaybeOk, {}, {}};
}
// check to see if the operand types are resolved enough, and wait to reduce if not
// if any of them are `never`, the intersection will always be `never`, so we can reduce directly.
@ -2203,7 +2260,7 @@ TypeFunctionReductionResult<TypeId> intersectTypeFunction(
for (auto ty : types)
{
// skip any `*no-refine*` types.
if (FFlag::LuauRemoveNotAnyHack && get<NoRefineType>(ty))
if (get<NoRefineType>(ty))
continue;
SimplifyResult result = simplifyIntersection(ctx->builtins, ctx->arena, resultTy, ty);
@ -2722,6 +2779,215 @@ TypeFunctionReductionResult<TypeId> rawgetTypeFunction(
return indexFunctionImpl(typeParams, packParams, ctx, /* isRaw */ true);
}
TypeFunctionReductionResult<TypeId> setmetatableTypeFunction(
TypeId instance,
const std::vector<TypeId>& typeParams,
const std::vector<TypePackId>& packParams,
NotNull<TypeFunctionContext> ctx
)
{
if (typeParams.size() != 2 || !packParams.empty())
{
ctx->ice->ice("setmetatable type function: encountered a type function instance without the required argument structure");
LUAU_ASSERT(false);
}
const Location location = ctx->constraint ? ctx->constraint->location : Location{};
TypeId targetTy = follow(typeParams.at(0));
TypeId metatableTy = follow(typeParams.at(1));
std::shared_ptr<const NormalizedType> targetNorm = ctx->normalizer->normalize(targetTy);
// if the operand failed to normalize, we can't reduce, but know nothing about inhabitance.
if (!targetNorm)
return {std::nullopt, Reduction::MaybeOk, {}, {}};
// cannot setmetatable on something without table parts.
if (!targetNorm->hasTables())
return {std::nullopt, Reduction::Erroneous, {}, {}};
// we're trying to reject any type that has not normalized to a table or a union/intersection of tables.
if (targetNorm->hasTops() || targetNorm->hasBooleans() || targetNorm->hasErrors() || targetNorm->hasNils() ||
targetNorm->hasNumbers() || targetNorm->hasStrings() || targetNorm->hasThreads() || targetNorm->hasBuffers() ||
targetNorm->hasFunctions() || targetNorm->hasTyvars() || targetNorm->hasClasses())
return {std::nullopt, Reduction::Erroneous, {}, {}};
// if the supposed metatable is not a table, we will fail to reduce.
if (!get<TableType>(metatableTy) && !get<MetatableType>(metatableTy))
return {std::nullopt, Reduction::Erroneous, {}, {}};
if (targetNorm->tables.size() == 1)
{
TypeId table = *targetNorm->tables.begin();
// findMetatableEntry demands the ability to emit errors, so we must give it
// the necessary state to do that, even if we intend to just eat the errors.
ErrorVec dummy;
std::optional<TypeId> metatableMetamethod = findMetatableEntry(ctx->builtins, dummy, table, "__metatable", location);
// if the `__metatable` metamethod is present, then the table is locked and we cannot `setmetatable` on it.
if (metatableMetamethod)
return {std::nullopt, Reduction::Erroneous, {}, {}};
TypeId withMetatable = ctx->arena->addType(MetatableType{table, metatableTy});
return {withMetatable, Reduction::MaybeOk, {}, {}};
}
TypeId result = ctx->builtins->neverType;
for (auto componentTy : targetNorm->tables)
{
// findMetatableEntry demands the ability to emit errors, so we must give it
// the necessary state to do that, even if we intend to just eat the errors.
ErrorVec dummy;
std::optional<TypeId> metatableMetamethod = findMetatableEntry(ctx->builtins, dummy, componentTy, "__metatable", location);
// if the `__metatable` metamethod is present, then the table is locked and we cannot `setmetatable` on it.
if (metatableMetamethod)
return {std::nullopt, Reduction::Erroneous, {}, {}};
TypeId withMetatable = ctx->arena->addType(MetatableType{componentTy, metatableTy});
SimplifyResult simplified = simplifyUnion(ctx->builtins, ctx->arena, result, withMetatable);
if (!simplified.blockedTypes.empty())
{
std::vector<TypeId> blockedTypes{};
blockedTypes.reserve(simplified.blockedTypes.size());
for (auto ty : simplified.blockedTypes)
blockedTypes.push_back(ty);
return {std::nullopt, Reduction::MaybeOk, blockedTypes, {}};
}
result = simplified.result;
}
return {result, Reduction::MaybeOk, {}, {}};
}
static TypeFunctionReductionResult<TypeId> getmetatableHelper(
TypeId targetTy,
const Location& location,
NotNull<TypeFunctionContext> ctx
)
{
targetTy = follow(targetTy);
std::optional<TypeId> metatable = std::nullopt;
bool erroneous = true;
if (auto table = get<TableType>(targetTy))
erroneous = false;
if (auto mt = get<MetatableType>(targetTy))
{
metatable = mt->metatable;
erroneous = false;
}
if (auto clazz = get<ClassType>(targetTy))
{
metatable = clazz->metatable;
erroneous = false;
}
if (auto primitive = get<PrimitiveType>(targetTy))
{
metatable = primitive->metatable;
erroneous = false;
}
if (auto singleton = get<SingletonType>(targetTy))
{
if (get<StringSingleton>(singleton))
{
auto primitiveString = get<PrimitiveType>(ctx->builtins->stringType);
metatable = primitiveString->metatable;
}
erroneous = false;
}
if (erroneous)
return {std::nullopt, Reduction::Erroneous, {}, {}};
// findMetatableEntry demands the ability to emit errors, so we must give it
// the necessary state to do that, even if we intend to just eat the errors.
ErrorVec dummy;
std::optional<TypeId> metatableMetamethod = findMetatableEntry(ctx->builtins, dummy, targetTy, "__metatable", location);
if (metatableMetamethod)
return {metatableMetamethod, Reduction::MaybeOk, {}, {}};
if (metatable)
return {metatable, Reduction::MaybeOk, {}, {}};
return {ctx->builtins->nilType, Reduction::MaybeOk, {}, {}};
}
TypeFunctionReductionResult<TypeId> getmetatableTypeFunction(
TypeId instance,
const std::vector<TypeId>& typeParams,
const std::vector<TypePackId>& packParams,
NotNull<TypeFunctionContext> ctx
)
{
if (typeParams.size() != 1 || !packParams.empty())
{
ctx->ice->ice("getmetatable type function: encountered a type function instance without the required argument structure");
LUAU_ASSERT(false);
}
const Location location = ctx->constraint ? ctx->constraint->location : Location{};
TypeId targetTy = follow(typeParams.at(0));
if (isPending(targetTy, ctx->solver))
return {std::nullopt, Reduction::MaybeOk, {targetTy}, {}};
if (auto ut = get<UnionType>(targetTy))
{
std::vector<TypeId> options{};
options.reserve(ut->options.size());
for (auto option : ut->options)
{
TypeFunctionReductionResult<TypeId> result = getmetatableHelper(option, location, ctx);
if (!result.result)
return result;
options.push_back(*result.result);
}
return {ctx->arena->addType(UnionType{std::move(options)}), Reduction::MaybeOk, {}, {}};
}
if (auto it = get<IntersectionType>(targetTy))
{
std::vector<TypeId> parts{};
parts.reserve(it->parts.size());
for (auto part : it->parts)
{
TypeFunctionReductionResult<TypeId> result = getmetatableHelper(part, location, ctx);
if (!result.result)
return result;
parts.push_back(*result.result);
}
return {ctx->arena->addType(IntersectionType{std::move(parts)}), Reduction::MaybeOk, {}, {}};
}
return getmetatableHelper(targetTy, location, ctx);
}
BuiltinTypeFunctions::BuiltinTypeFunctions()
: userFunc{"user", userDefinedTypeFunction}
, notFunc{"not", notTypeFunction}
@ -2748,6 +3014,8 @@ BuiltinTypeFunctions::BuiltinTypeFunctions()
, rawkeyofFunc{"rawkeyof", rawkeyofTypeFunction}
, indexFunc{"index", indexTypeFunction}
, rawgetFunc{"rawget", rawgetTypeFunction}
, setmetatableFunc{"setmetatable", setmetatableTypeFunction}
, getmetatableFunc{"getmetatable", getmetatableTypeFunction}
{
}
@ -2794,6 +3062,12 @@ void BuiltinTypeFunctions::addToScope(NotNull<TypeArena> arena, NotNull<Scope> s
scope->exportedTypeBindings[indexFunc.name] = mkBinaryTypeFunction(&indexFunc);
scope->exportedTypeBindings[rawgetFunc.name] = mkBinaryTypeFunction(&rawgetFunc);
if (FFlag::LuauMetatableTypeFunctions)
{
scope->exportedTypeBindings[setmetatableFunc.name] = mkBinaryTypeFunction(&setmetatableFunc);
scope->exportedTypeBindings[getmetatableFunc.name] = mkUnaryTypeFunction(&getmetatableFunc);
}
}
const BuiltinTypeFunctions& builtinTypeFunctions()

View file

@ -15,7 +15,6 @@
LUAU_DYNAMIC_FASTINT(LuauTypeFunctionSerdeIterationLimit)
LUAU_FASTFLAGVARIABLE(LuauUserTypeFunFixInner)
LUAU_FASTFLAGVARIABLE(LuauUserTypeFunFixNoReadWrite)
LUAU_FASTFLAGVARIABLE(LuauUserTypeFunGenerics)
LUAU_FASTFLAGVARIABLE(LuauUserTypeFunCloneTail)
@ -683,10 +682,8 @@ static int readTableProp(lua_State* L)
auto prop = tftt->props.at(tfsst->value);
if (prop.readTy)
allocTypeUserData(L, (*prop.readTy)->type);
else if (FFlag::LuauUserTypeFunFixNoReadWrite)
lua_pushnil(L);
else
luaL_error(L, "type.readproperty: property %s is write-only, and therefore does not have a read type.", tfsst->value.c_str());
lua_pushnil(L);
return 1;
}
@ -723,10 +720,8 @@ static int writeTableProp(lua_State* L)
auto prop = tftt->props.at(tfsst->value);
if (prop.writeTy)
allocTypeUserData(L, (*prop.writeTy)->type);
else if (FFlag::LuauUserTypeFunFixNoReadWrite)
lua_pushnil(L);
else
luaL_error(L, "type.writeproperty: property %s is read-only, and therefore does not have a write type.", tfsst->value.c_str());
lua_pushnil(L);
return 1;
}

View file

@ -33,6 +33,8 @@ LUAU_FASTFLAG(LuauKnowsTheDataModel3)
LUAU_FASTFLAGVARIABLE(DebugLuauFreezeDuringUnification)
LUAU_FASTFLAG(LuauInstantiateInSubtyping)
LUAU_FASTFLAGVARIABLE(LuauOldSolverCreatesChildScopePointers)
LUAU_FASTFLAG(LuauPreserveUnionIntersectionNodeForLeadingTokenSingleType)
LUAU_FASTFLAG(LuauFreeTypesMustHaveBounds)
namespace Luau
{
@ -761,8 +763,12 @@ ControlFlow TypeChecker::check(const ScopePtr& scope, const AstStatRepeat& state
struct Demoter : Substitution
{
Demoter(TypeArena* arena)
TypeArena* arena = nullptr;
NotNull<BuiltinTypes> builtins;
Demoter(TypeArena* arena, NotNull<BuiltinTypes> builtins)
: Substitution(TxnLog::empty(), arena)
, arena(arena)
, builtins(builtins)
{
}
@ -788,7 +794,8 @@ struct Demoter : Substitution
{
auto ftv = get<FreeType>(ty);
LUAU_ASSERT(ftv);
return addType(FreeType{demotedLevel(ftv->level)});
return FFlag::LuauFreeTypesMustHaveBounds ? arena->freshType(builtins, demotedLevel(ftv->level))
: addType(FreeType{demotedLevel(ftv->level)});
}
TypePackId clean(TypePackId tp) override
@ -835,7 +842,7 @@ ControlFlow TypeChecker::check(const ScopePtr& scope, const AstStatReturn& retur
}
}
Demoter demoter{&currentModule->internalTypes};
Demoter demoter{&currentModule->internalTypes, builtinTypes};
demoter.demote(expectedTypes);
TypePackId retPack = checkExprList(scope, return_.location, return_.list, false, {}, expectedTypes).type;
@ -4408,7 +4415,7 @@ std::vector<std::optional<TypeId>> TypeChecker::getExpectedTypesForCall(const st
}
}
Demoter demoter{&currentModule->internalTypes};
Demoter demoter{&currentModule->internalTypes, builtinTypes};
demoter.demote(expectedTypes);
return expectedTypes;
@ -5273,7 +5280,8 @@ TypeId TypeChecker::freshType(const ScopePtr& scope)
TypeId TypeChecker::freshType(TypeLevel level)
{
return currentModule->internalTypes.addType(Type(FreeType(level)));
return FFlag::LuauFreeTypesMustHaveBounds ? currentModule->internalTypes.freshType(builtinTypes, level)
: currentModule->internalTypes.addType(Type(FreeType(level)));
}
TypeId TypeChecker::singletonType(bool value)
@ -5718,6 +5726,12 @@ TypeId TypeChecker::resolveTypeWorker(const ScopePtr& scope, const AstType& anno
}
else if (const auto& un = annotation.as<AstTypeUnion>())
{
if (FFlag::LuauPreserveUnionIntersectionNodeForLeadingTokenSingleType)
{
if (un->types.size == 1)
return resolveType(scope, *un->types.data[0]);
}
std::vector<TypeId> types;
for (AstType* ann : un->types)
types.push_back(resolveType(scope, *ann));
@ -5726,12 +5740,22 @@ TypeId TypeChecker::resolveTypeWorker(const ScopePtr& scope, const AstType& anno
}
else if (const auto& un = annotation.as<AstTypeIntersection>())
{
if (FFlag::LuauPreserveUnionIntersectionNodeForLeadingTokenSingleType)
{
if (un->types.size == 1)
return resolveType(scope, *un->types.data[0]);
}
std::vector<TypeId> types;
for (AstType* ann : un->types)
types.push_back(resolveType(scope, *ann));
return addType(IntersectionType{types});
}
else if (const auto& g = annotation.as<AstTypeGroup>())
{
return resolveType(scope, *g->type);
}
else if (const auto& tsb = annotation.as<AstTypeSingletonBool>())
{
return singletonType(tsb->value);

View file

@ -5,6 +5,7 @@
#include "Luau/Normalize.h"
#include "Luau/Scope.h"
#include "Luau/ToString.h"
#include "Luau/Type.h"
#include "Luau/TypeInfer.h"
#include <algorithm>
@ -12,6 +13,7 @@
LUAU_FASTFLAG(LuauSolverV2);
LUAU_FASTFLAG(LuauAutocompleteRefactorsForIncrementalAutocomplete);
LUAU_FASTFLAG(LuauTrackInteriorFreeTypesOnScope);
LUAU_FASTFLAG(LuauFreeTypesMustHaveBounds)
namespace Luau
{
@ -323,7 +325,7 @@ TypePack extendTypePack(
trackInteriorFreeType(ftp->scope, t);
}
else
t = arena.freshType(ftp->scope);
t = FFlag::LuauFreeTypesMustHaveBounds ? arena.freshType(builtinTypes, ftp->scope) : arena.freshType_DEPRECATED(ftp->scope);
}
newPack.head.push_back(t);

View file

@ -22,6 +22,7 @@ LUAU_FASTFLAGVARIABLE(LuauTransitiveSubtyping)
LUAU_FASTFLAG(LuauSolverV2)
LUAU_FASTFLAGVARIABLE(LuauFixIndexerSubtypingOrdering)
LUAU_FASTFLAGVARIABLE(LuauUnifierRecursionOnRestart)
LUAU_FASTFLAG(LuauFreeTypesMustHaveBounds)
namespace Luau
{
@ -1648,7 +1649,7 @@ void Unifier::tryUnify_(TypePackId subTp, TypePackId superTp, bool isFunctionCal
if (FFlag::LuauSolverV2)
return freshType(NotNull{types}, builtinTypes, scope);
else
return types->freshType(scope, level);
return FFlag::LuauFreeTypesMustHaveBounds ? types->freshType(builtinTypes, scope, level) : types->freshType_DEPRECATED(scope, level);
};
const TypePackId emptyTp = types->addTypePack(TypePack{{}, std::nullopt});

View file

@ -1204,6 +1204,18 @@ public:
const AstArray<char> value;
};
class AstTypeGroup : public AstType
{
public:
LUAU_RTTI(AstTypeGroup)
explicit AstTypeGroup(const Location& location, AstType* type);
void visit(AstVisitor* visitor) override;
AstType* type;
};
class AstTypePack : public AstNode
{
public:
@ -1470,6 +1482,10 @@ public:
{
return visit(static_cast<AstType*>(node));
}
virtual bool visit(class AstTypeGroup* node)
{
return visit(static_cast<AstType*>(node));
}
virtual bool visit(class AstTypeError* node)
{
return visit(static_cast<AstType*>(node));

334
Ast/include/Luau/Cst.h Normal file
View file

@ -0,0 +1,334 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#pragma once
#include "Luau/Location.h"
#include <string>
namespace Luau
{
extern int gCstRttiIndex;
template<typename T>
struct CstRtti
{
static const int value;
};
template<typename T>
const int CstRtti<T>::value = ++gCstRttiIndex;
#define LUAU_CST_RTTI(Class) \
static int CstClassIndex() \
{ \
return CstRtti<Class>::value; \
}
class CstNode
{
public:
explicit CstNode(int classIndex)
: classIndex(classIndex)
{
}
template<typename T>
bool is() const
{
return classIndex == T::CstClassIndex();
}
template<typename T>
T* as()
{
return classIndex == T::CstClassIndex() ? static_cast<T*>(this) : nullptr;
}
template<typename T>
const T* as() const
{
return classIndex == T::CstClassIndex() ? static_cast<const T*>(this) : nullptr;
}
const int classIndex;
};
class CstExprConstantNumber : public CstNode
{
public:
LUAU_CST_RTTI(CstExprConstantNumber)
explicit CstExprConstantNumber(const AstArray<char>& value);
AstArray<char> value;
};
class CstExprConstantString : public CstNode
{
public:
LUAU_CST_RTTI(CstExprConstantNumber)
enum QuoteStyle
{
QuotedSingle,
QuotedDouble,
QuotedRaw,
QuotedInterp,
};
CstExprConstantString(AstArray<char> sourceString, QuoteStyle quoteStyle, unsigned int blockDepth);
AstArray<char> sourceString;
QuoteStyle quoteStyle;
unsigned int blockDepth;
};
class CstExprCall : public CstNode
{
public:
LUAU_CST_RTTI(CstExprCall)
CstExprCall(std::optional<Position> openParens, std::optional<Position> closeParens, AstArray<Position> commaPositions);
std::optional<Position> openParens;
std::optional<Position> closeParens;
AstArray<Position> commaPositions;
};
class CstExprIndexExpr : public CstNode
{
public:
LUAU_CST_RTTI(CstExprIndexExpr)
CstExprIndexExpr(Position openBracketPosition, Position closeBracketPosition);
Position openBracketPosition;
Position closeBracketPosition;
};
class CstExprTable : public CstNode
{
public:
LUAU_CST_RTTI(CstExprTable)
enum Separator
{
Comma,
Semicolon,
};
struct Item
{
std::optional<Position> indexerOpenPosition; // '[', only if Kind == General
std::optional<Position> indexerClosePosition; // ']', only if Kind == General
std::optional<Position> equalsPosition; // only if Kind != List
std::optional<Separator> separator; // may be missing for last Item
std::optional<Position> separatorPosition;
};
explicit CstExprTable(const AstArray<Item>& items);
AstArray<Item> items;
};
// TODO: Shared between unary and binary, should we split?
class CstExprOp : public CstNode
{
public:
LUAU_CST_RTTI(CstExprOp)
explicit CstExprOp(Position opPosition);
Position opPosition;
};
class CstExprIfElse : public CstNode
{
public:
LUAU_CST_RTTI(CstExprIfElse)
CstExprIfElse(Position thenPosition, Position elsePosition, bool isElseIf);
Position thenPosition;
Position elsePosition;
bool isElseIf;
};
class CstExprInterpString : public CstNode
{
public:
LUAU_CST_RTTI(CstExprInterpString)
explicit CstExprInterpString(AstArray<AstArray<char>> sourceStrings, AstArray<Position> stringPositions);
AstArray<AstArray<char>> sourceStrings;
AstArray<Position> stringPositions;
};
class CstStatDo : public CstNode
{
public:
LUAU_CST_RTTI(CstStatDo)
explicit CstStatDo(Position endPosition);
Position endPosition;
};
class CstStatRepeat : public CstNode
{
public:
LUAU_CST_RTTI(CstStatRepeat)
explicit CstStatRepeat(Position untilPosition);
Position untilPosition;
};
class CstStatReturn : public CstNode
{
public:
LUAU_CST_RTTI(CstStatReturn)
explicit CstStatReturn(AstArray<Position> commaPositions);
AstArray<Position> commaPositions;
};
class CstStatLocal : public CstNode
{
public:
LUAU_CST_RTTI(CstStatLocal)
CstStatLocal(AstArray<Position> varsCommaPositions, AstArray<Position> valuesCommaPositions);
AstArray<Position> varsCommaPositions;
AstArray<Position> valuesCommaPositions;
};
class CstStatFor : public CstNode
{
public:
LUAU_CST_RTTI(CstStatFor)
CstStatFor(Position equalsPosition, Position endCommaPosition, std::optional<Position> stepCommaPosition);
Position equalsPosition;
Position endCommaPosition;
std::optional<Position> stepCommaPosition;
};
class CstStatForIn : public CstNode
{
public:
LUAU_CST_RTTI(CstStatForIn)
CstStatForIn(AstArray<Position> varsCommaPositions, AstArray<Position> valuesCommaPositions);
AstArray<Position> varsCommaPositions;
AstArray<Position> valuesCommaPositions;
};
class CstStatAssign : public CstNode
{
public:
LUAU_CST_RTTI(CstStatAssign)
CstStatAssign(AstArray<Position> varsCommaPositions, Position equalsPosition, AstArray<Position> valuesCommaPositions);
AstArray<Position> varsCommaPositions;
Position equalsPosition;
AstArray<Position> valuesCommaPositions;
};
class CstStatCompoundAssign : public CstNode
{
public:
LUAU_CST_RTTI(CstStatCompoundAssign)
explicit CstStatCompoundAssign(Position opPosition);
Position opPosition;
};
class CstStatLocalFunction : public CstNode
{
public:
LUAU_CST_RTTI(CstStatLocalFunction)
explicit CstStatLocalFunction(Position functionKeywordPosition);
Position functionKeywordPosition;
};
class CstTypeReference : public CstNode
{
public:
LUAU_CST_RTTI(CstTypeReference)
CstTypeReference(
std::optional<Position> prefixPointPosition,
Position openParametersPosition,
AstArray<Position> parametersCommaPositions,
Position closeParametersPosition
);
std::optional<Position> prefixPointPosition;
Position openParametersPosition;
AstArray<Position> parametersCommaPositions;
Position closeParametersPosition;
};
class CstTypeTable : public CstNode
{
public:
LUAU_CST_RTTI(CstTypeTable)
struct Item
{
enum struct Kind
{
Indexer,
Property,
StringProperty,
};
Kind kind;
Position indexerOpenPosition; // '[', only if Kind != Property
Position indexerClosePosition; // ']' only if Kind != Property
Position colonPosition;
std::optional<CstExprTable::Separator> separator; // may be missing for last Item
std::optional<Position> separatorPosition;
CstExprConstantString* stringInfo = nullptr; // only if Kind == StringProperty
};
CstTypeTable(AstArray<Item> items, bool isArray);
AstArray<Item> items;
bool isArray = false;
};
class CstTypeTypeof : public CstNode
{
public:
LUAU_CST_RTTI(CstTypeTypeof)
CstTypeTypeof(Position openPosition, Position closePosition);
Position openPosition;
Position closePosition;
};
class CstTypeSingletonString : public CstNode
{
public:
LUAU_CST_RTTI(CstTypeSingletonString)
CstTypeSingletonString(AstArray<char> sourceString, CstExprConstantString::QuoteStyle quoteStyle, unsigned int blockDepth);
AstArray<char> sourceString;
CstExprConstantString::QuoteStyle quoteStyle;
unsigned int blockDepth;
};
} // namespace Luau

View file

@ -87,6 +87,12 @@ struct Lexeme
Reserved_END
};
enum struct QuoteStyle
{
Single,
Double,
};
Type type;
Location location;
@ -111,6 +117,8 @@ public:
Lexeme(const Location& location, Type type, const char* name);
unsigned int getLength() const;
unsigned int getBlockDepth() const;
QuoteStyle getQuoteStyle() const;
std::string toString() const;
};

View file

@ -29,6 +29,8 @@ struct ParseOptions
bool allowDeclarationSyntax = false;
bool captureComments = false;
std::optional<FragmentParseResumeSettings> parseFragment = std::nullopt;
bool storeCstData = false;
bool noErrorLimit = false;
};
} // namespace Luau

View file

@ -10,6 +10,7 @@ namespace Luau
{
class AstStatBlock;
class CstNode;
class ParseError : public std::exception
{
@ -55,6 +56,8 @@ struct Comment
Location location;
};
using CstNodeMap = DenseHashMap<AstNode*, CstNode*>;
struct ParseResult
{
AstStatBlock* root;
@ -64,6 +67,8 @@ struct ParseResult
std::vector<ParseError> errors;
std::vector<Comment> commentLocations;
CstNodeMap cstNodeMap{nullptr};
};
static constexpr const char* kParseNameError = "%error-id%";

View file

@ -8,6 +8,7 @@
#include "Luau/StringUtils.h"
#include "Luau/DenseHash.h"
#include "Luau/Common.h"
#include "Luau/Cst.h"
#include <initializer_list>
#include <optional>
@ -173,14 +174,18 @@ private:
);
// explist ::= {exp `,'} exp
void parseExprList(TempVector<AstExpr*>& result);
void parseExprList(TempVector<AstExpr*>& result, TempVector<Position>* commaPositions = nullptr);
// binding ::= Name [`:` Type]
Binding parseBinding();
// bindinglist ::= (binding | `...') {`,' bindinglist}
// Returns the location of the vararg ..., or std::nullopt if the function is not vararg.
std::tuple<bool, Location, AstTypePack*> parseBindingList(TempVector<Binding>& result, bool allowDot3 = false);
std::tuple<bool, Location, AstTypePack*> parseBindingList(
TempVector<Binding>& result,
bool allowDot3 = false,
TempVector<Position>* commaPositions = nullptr
);
AstType* parseOptionalType();
@ -201,7 +206,17 @@ private:
std::optional<AstTypeList> parseOptionalReturnType();
std::pair<Location, AstTypeList> parseReturnType();
AstTableIndexer* parseTableIndexer(AstTableAccess access, std::optional<Location> accessLocation);
struct TableIndexerResult
{
AstTableIndexer* node;
Position indexerOpenPosition;
Position indexerClosePosition;
Position colonPosition;
};
TableIndexerResult parseTableIndexer(AstTableAccess access, std::optional<Location> accessLocation);
// Remove with FFlagLuauStoreCSTData
AstTableIndexer* parseTableIndexer_DEPRECATED(AstTableAccess access, std::optional<Location> accessLocation);
AstTypeOrPack parseFunctionType(bool allowPack, const AstArray<AstAttr*>& attributes);
AstType* parseFunctionTypeTail(
@ -259,6 +274,8 @@ private:
// args ::= `(' [explist] `)' | tableconstructor | String
AstExpr* parseFunctionArgs(AstExpr* func, bool self);
std::optional<CstExprTable::Separator> tableSeparator();
// tableconstructor ::= `{' [fieldlist] `}'
// fieldlist ::= field {fieldsep field} [fieldsep]
// field ::= `[' exp `]' `=' exp | Name `=' exp | exp
@ -280,9 +297,13 @@ private:
std::pair<AstArray<AstGenericType>, AstArray<AstGenericTypePack>> parseGenericTypeList(bool withDefaultValues);
// `<' Type[, ...] `>'
AstArray<AstTypeOrPack> parseTypeParams();
AstArray<AstTypeOrPack> parseTypeParams(
Position* openingPosition = nullptr,
TempVector<Position>* commaPositions = nullptr,
Position* closingPosition = nullptr
);
std::optional<AstArray<char>> parseCharArray();
std::optional<AstArray<char>> parseCharArray(AstArray<char>* originalString = nullptr);
AstExpr* parseString();
AstExpr* parseNumber();
@ -292,6 +313,9 @@ private:
void restoreLocals(unsigned int offset);
/// Returns string quote style and block depth
std::pair<CstExprConstantString::QuoteStyle, unsigned int> extractStringDetails();
// check that parser is at lexeme/symbol, move to next lexeme/symbol on success, report failure and continue on failure
bool expectAndConsume(char value, const char* context = nullptr);
bool expectAndConsume(Lexeme::Type type, const char* context = nullptr);
@ -435,6 +459,7 @@ private:
std::vector<AstAttr*> scratchAttr;
std::vector<AstStat*> scratchStat;
std::vector<AstArray<char>> scratchString;
std::vector<AstArray<char>> scratchString2;
std::vector<AstExpr*> scratchExpr;
std::vector<AstExpr*> scratchExprAux;
std::vector<AstName> scratchName;
@ -442,15 +467,20 @@ private:
std::vector<Binding> scratchBinding;
std::vector<AstLocal*> scratchLocal;
std::vector<AstTableProp> scratchTableTypeProps;
std::vector<CstTypeTable::Item> scratchCstTableTypeProps;
std::vector<AstType*> scratchType;
std::vector<AstTypeOrPack> scratchTypeOrPack;
std::vector<AstDeclaredClassProp> scratchDeclaredClassProps;
std::vector<AstExprTable::Item> scratchItem;
std::vector<CstExprTable::Item> scratchCstItem;
std::vector<AstArgumentName> scratchArgName;
std::vector<AstGenericType> scratchGenericTypes;
std::vector<AstGenericTypePack> scratchGenericTypePacks;
std::vector<std::optional<AstArgumentName>> scratchOptArgName;
std::vector<Position> scratchPosition;
std::string scratchData;
CstNodeMap cstNodeMap;
};
} // namespace Luau

View file

@ -1091,6 +1091,18 @@ void AstTypeSingletonString::visit(AstVisitor* visitor)
visitor->visit(this);
}
AstTypeGroup::AstTypeGroup(const Location& location, AstType* type)
: AstType(ClassIndex(), location)
, type(type)
{
}
void AstTypeGroup::visit(AstVisitor* visitor)
{
if (visitor->visit(this))
type->visit(visitor);
}
AstTypeError::AstTypeError(const Location& location, const AstArray<AstType*>& types, bool isMissing, unsigned messageIndex)
: AstType(ClassIndex(), location)
, types(types)

169
Ast/src/Cst.cpp Normal file
View file

@ -0,0 +1,169 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#include "Luau/Ast.h"
#include "Luau/Cst.h"
#include "Luau/Common.h"
namespace Luau
{
int gCstRttiIndex = 0;
CstExprConstantNumber::CstExprConstantNumber(const AstArray<char>& value)
: CstNode(CstClassIndex())
, value(value)
{
}
CstExprConstantString::CstExprConstantString(AstArray<char> sourceString, QuoteStyle quoteStyle, unsigned int blockDepth)
: CstNode(CstClassIndex())
, sourceString(sourceString)
, quoteStyle(quoteStyle)
, blockDepth(blockDepth)
{
LUAU_ASSERT(blockDepth == 0 || quoteStyle == QuoteStyle::QuotedRaw);
}
CstExprCall::CstExprCall(std::optional<Position> openParens, std::optional<Position> closeParens, AstArray<Position> commaPositions)
: CstNode(CstClassIndex())
, openParens(openParens)
, closeParens(closeParens)
, commaPositions(commaPositions)
{
}
CstExprIndexExpr::CstExprIndexExpr(Position openBracketPosition, Position closeBracketPosition)
: CstNode(CstClassIndex())
, openBracketPosition(openBracketPosition)
, closeBracketPosition(closeBracketPosition)
{
}
CstExprTable::CstExprTable(const AstArray<Item>& items)
: CstNode(CstClassIndex())
, items(items)
{
}
CstExprOp::CstExprOp(Position opPosition)
: CstNode(CstClassIndex())
, opPosition(opPosition)
{
}
CstExprIfElse::CstExprIfElse(Position thenPosition, Position elsePosition, bool isElseIf)
: CstNode(CstClassIndex())
, thenPosition(thenPosition)
, elsePosition(elsePosition)
, isElseIf(isElseIf)
{
}
CstExprInterpString::CstExprInterpString(AstArray<AstArray<char>> sourceStrings, AstArray<Position> stringPositions)
: CstNode(CstClassIndex())
, sourceStrings(sourceStrings)
, stringPositions(stringPositions)
{
}
CstStatDo::CstStatDo(Position endPosition)
: CstNode(CstClassIndex())
, endPosition(endPosition)
{
}
CstStatRepeat::CstStatRepeat(Position untilPosition)
: CstNode(CstClassIndex())
, untilPosition(untilPosition)
{
}
CstStatReturn::CstStatReturn(AstArray<Position> commaPositions)
: CstNode(CstClassIndex())
, commaPositions(commaPositions)
{
}
CstStatLocal::CstStatLocal(AstArray<Position> varsCommaPositions, AstArray<Position> valuesCommaPositions)
: CstNode(CstClassIndex())
, varsCommaPositions(varsCommaPositions)
, valuesCommaPositions(valuesCommaPositions)
{
}
CstStatFor::CstStatFor(Position equalsPosition, Position endCommaPosition, std::optional<Position> stepCommaPosition)
: CstNode(CstClassIndex())
, equalsPosition(equalsPosition)
, endCommaPosition(endCommaPosition)
, stepCommaPosition(stepCommaPosition)
{
}
CstStatForIn::CstStatForIn(AstArray<Position> varsCommaPositions, AstArray<Position> valuesCommaPositions)
: CstNode(CstClassIndex())
, varsCommaPositions(varsCommaPositions)
, valuesCommaPositions(valuesCommaPositions)
{
}
CstStatAssign::CstStatAssign(
AstArray<Position> varsCommaPositions,
Position equalsPosition,
AstArray<Position> valuesCommaPositions
)
: CstNode(CstClassIndex())
, varsCommaPositions(varsCommaPositions)
, equalsPosition(equalsPosition)
, valuesCommaPositions(valuesCommaPositions)
{
}
CstStatCompoundAssign::CstStatCompoundAssign(Position opPosition)
: CstNode(CstClassIndex())
, opPosition(opPosition)
{
}
CstStatLocalFunction::CstStatLocalFunction(Position functionKeywordPosition)
: CstNode(CstClassIndex())
, functionKeywordPosition(functionKeywordPosition)
{
}
CstTypeReference::CstTypeReference(
std::optional<Position> prefixPointPosition,
Position openParametersPosition,
AstArray<Position> parametersCommaPositions,
Position closeParametersPosition
)
: CstNode(CstClassIndex())
, prefixPointPosition(prefixPointPosition)
, openParametersPosition(openParametersPosition)
, parametersCommaPositions(parametersCommaPositions)
, closeParametersPosition(closeParametersPosition)
{
}
CstTypeTable::CstTypeTable(AstArray<Item> items, bool isArray)
: CstNode(CstClassIndex())
, items(items)
, isArray(isArray)
{
}
CstTypeTypeof::CstTypeTypeof(Position openPosition, Position closePosition)
: CstNode(CstClassIndex())
, openPosition(openPosition)
, closePosition(closePosition)
{
}
CstTypeSingletonString::CstTypeSingletonString(AstArray<char> sourceString, CstExprConstantString::QuoteStyle quoteStyle, unsigned int blockDepth)
: CstNode(CstClassIndex())
, sourceString(sourceString)
, quoteStyle(quoteStyle)
, blockDepth(blockDepth)
{
LUAU_ASSERT(quoteStyle != CstExprConstantString::QuotedInterp);
}
} // namespace Luau

View file

@ -306,6 +306,38 @@ static char unescape(char ch)
}
}
unsigned int Lexeme::getBlockDepth() const
{
LUAU_ASSERT(type == Lexeme::RawString || type == Lexeme::BlockComment);
// If we have a well-formed string, we are guaranteed to see 2 `]` characters after the end of the string contents
LUAU_ASSERT(*(data + length) == ']');
unsigned int depth = 0;
do
{
depth++;
} while (*(data + length + depth) != ']');
return depth - 1;
}
Lexeme::QuoteStyle Lexeme::getQuoteStyle() const
{
LUAU_ASSERT(type == Lexeme::QuotedString);
// If we have a well-formed string, we are guaranteed to see a closing delimiter after the string
LUAU_ASSERT(data);
char quote = *(data + length);
if (quote == '\'')
return Lexeme::QuoteStyle::Single;
else if (quote == '"')
return Lexeme::QuoteStyle::Double;
LUAU_ASSERT(!"Unknown quote style");
return Lexeme::QuoteStyle::Double; // unreachable, but required due to compiler warning
}
Lexer::Lexer(const char* buffer, size_t bufferSize, AstNameTable& names, Position startPosition)
: buffer(buffer)
, bufferSize(bufferSize)

File diff suppressed because it is too large Load diff

View file

@ -20,9 +20,21 @@
#elif defined(__linux__) || defined(__APPLE__)
// Defined in unwind.h which may not be easily discoverable on various platforms
extern "C" void __register_frame(const void*) __attribute__((weak));
extern "C" void __deregister_frame(const void*) __attribute__((weak));
// __register_frame and __deregister_frame are defined in libgcc or libc++
// (depending on how it's built). We want to declare them as weak symbols
// so that if they're provided by a shared library, we'll use them, and if
// not, we'll disable some c++ exception handling support. However, if they're
// declared as weak and the definitions are linked in a static library
// that's not linked with whole-archive, then the symbols will technically be defined here,
// and the linker won't look for the strong ones in the library.
#ifndef LUAU_ENABLE_REGISTER_FRAME
#define REGISTER_FRAME_WEAK __attribute__((weak))
#else
#define REGISTER_FRAME_WEAK
#endif
extern "C" void __register_frame(const void*) REGISTER_FRAME_WEAK;
extern "C" void __deregister_frame(const void*) REGISTER_FRAME_WEAK;
extern "C" void __unw_add_dynamic_fde() __attribute__((weak));
#endif
@ -121,7 +133,7 @@ void* createBlockUnwindInfo(void* context, uint8_t* block, size_t blockSize, siz
#endif
#elif defined(__linux__) || defined(__APPLE__)
if (!__register_frame)
if (!&__register_frame)
return nullptr;
visitFdeEntries(unwindData, __register_frame);
@ -150,7 +162,7 @@ void destroyBlockUnwindInfo(void* context, void* unwindData)
#endif
#elif defined(__linux__) || defined(__APPLE__)
if (!__deregister_frame)
if (!&__deregister_frame)
{
CODEGEN_ASSERT(!"Cannot deregister unwind information");
return;

View file

@ -14,7 +14,7 @@ inline bool isFlagExperimental(const char* flag)
"LuauInstantiateInSubtyping", // requires some fixes to lua-apps code
"LuauFixIndexerSubtypingOrdering", // requires some small fixes to lua-apps code since this fixes a false negative
"StudioReportLuauAny2", // takes telemetry data for usage of any types
"LuauTableCloneClonesType2", // requires fixes in lua-apps code, terrifyingly
"LuauTableCloneClonesType3", // requires fixes in lua-apps code, terrifyingly
"LuauSolverV2",
// makes sure we always have at least one entry
nullptr,

View file

@ -121,6 +121,10 @@ static LuauBytecodeType getType(
{
return LBC_TYPE_ANY;
}
else if (const AstTypeGroup* group = ty->as<AstTypeGroup>())
{
return getType(group->type, generics, typeAliases, resolveAliases, hostVectorType, userdataTypes, bytecode);
}
return LBC_TYPE_ANY;
}

View file

@ -17,6 +17,7 @@ target_sources(Luau.Ast PRIVATE
Ast/include/Luau/Allocator.h
Ast/include/Luau/Ast.h
Ast/include/Luau/Confusables.h
Ast/include/Luau/Cst.h
Ast/include/Luau/Lexer.h
Ast/include/Luau/Location.h
Ast/include/Luau/ParseOptions.h
@ -28,6 +29,7 @@ target_sources(Luau.Ast PRIVATE
Ast/src/Allocator.cpp
Ast/src/Ast.cpp
Ast/src/Confusables.cpp
Ast/src/Cst.cpp
Ast/src/Lexer.cpp
Ast/src/Location.cpp
Ast/src/Parser.cpp

View file

@ -270,7 +270,7 @@ static int buffer_readbits(lua_State* L)
uint64_t data = 0;
#if LUAU_BIG_ENDIAN
#if defined(LUAU_BIG_ENDIAN)
for (int i = int(endbyte) - 1; i >= int(startbyte); i--)
data = (data << 8) + uint8_t(((char*)buf)[i]);
#else
@ -306,7 +306,7 @@ static int buffer_writebits(lua_State* L)
uint64_t data = 0;
#if LUAU_BIG_ENDIAN
#if defined(LUAU_BIG_ENDIAN)
for (int i = int(endbyte) - 1; i >= int(startbyte); i--)
data = data * 256 + uint8_t(((char*)buf)[i]);
#else
@ -318,7 +318,7 @@ static int buffer_writebits(lua_State* L)
data = (data & ~mask) | ((uint64_t(value) << subbyteoffset) & mask);
#if LUAU_BIG_ENDIAN
#if defined(LUAU_BIG_ENDIAN)
for (int i = int(startbyte); i < int(endbyte); i++)
{
((char*)buf)[i] = data & 0xff;

View file

@ -20,7 +20,8 @@ LUAU_FASTFLAG(DebugLuauMagicTypes)
LUAU_FASTFLAG(StudioReportLuauAny2)
LUAU_FASTFLAG(LuauTrackInteriorFreeTypesOnScope)
LUAU_FASTFLAG(LuauAlwaysFillInFunctionCallDiscriminantTypes)
LUAU_FASTFLAG(LuauRemoveNotAnyHack)
LUAU_FASTFLAG(LuauStoreCSTData)
LUAU_FASTFLAG(LuauAstTypeGroup)
struct ATSFixture : BuiltinsFixture
@ -74,8 +75,23 @@ export type t8<t8> = t0 &(<t0 ...>(true | any)->(''))
LUAU_ASSERT(module->ats.typeInfo.size() == 1);
LUAU_ASSERT(module->ats.typeInfo[0].code == Pattern::Alias);
if (FFlag::LuauStoreCSTData && FFlag::LuauAstTypeGroup)
{
LUAU_ASSERT(module->ats.typeInfo[0].node == "export type t8<t8> = t0& (<t0...>( true | any)->(''))");
}
else if (FFlag::LuauStoreCSTData)
{
LUAU_ASSERT(module->ats.typeInfo[0].node == "export type t8<t8> = t0 &(<t0...>( true | any)->(''))");
}
else if (FFlag::LuauAstTypeGroup)
{
LUAU_ASSERT(module->ats.typeInfo[0].node == "export type t8<t8> = t0& (<t0 ...>(true | any)->(''))");
}
else
{
LUAU_ASSERT(module->ats.typeInfo[0].node == "export type t8<t8> = t0 &(<t0 ...>(true | any)->(''))");
}
}
TEST_CASE_FIXTURE(ATSFixture, "typepacks")
{
@ -413,7 +429,6 @@ TEST_CASE_FIXTURE(ATSFixture, "CannotExtendTable")
{FFlag::LuauSolverV2, true},
{FFlag::StudioReportLuauAny2, true},
{FFlag::LuauAlwaysFillInFunctionCallDiscriminantTypes, true},
{FFlag::LuauRemoveNotAnyHack, true},
};
fileResolver.source["game/Gui/Modules/A"] = R"(
@ -507,7 +522,6 @@ TEST_CASE_FIXTURE(ATSFixture, "racing_collision_2")
{FFlag::LuauSolverV2, true},
{FFlag::StudioReportLuauAny2, true},
{FFlag::LuauAlwaysFillInFunctionCallDiscriminantTypes, true},
{FFlag::LuauRemoveNotAnyHack, true},
};
fileResolver.source["game/Gui/Modules/A"] = R"(
@ -577,6 +591,18 @@ initialize()
LUAU_ASSERT(module->ats.typeInfo.size() == 11);
LUAU_ASSERT(module->ats.typeInfo[0].code == Pattern::FuncArg);
if (FFlag::LuauStoreCSTData)
{
CHECK_EQ(
module->ats.typeInfo[0].node,
"local function onCharacterAdded(character: Model)\n\n character.DescendantAdded:Connect(function(descendant)\n if "
"descendant:IsA('BasePart') then\n descendant.CollisionGroup = CHARACTER_COLLISION_GROUP\n end\n end)\n\n\n for _, descendant in "
"character:GetDescendants() do\n if descendant:IsA('BasePart') then\n descendant.CollisionGroup = CHARACTER_COLLISION_GROUP\n end\n "
"end\nend"
);
}
else
{
LUAU_ASSERT(
module->ats.typeInfo[0].node ==
"local function onCharacterAdded(character: Model)\n\n character.DescendantAdded:Connect(function(descendant)\n if "
@ -585,6 +611,7 @@ initialize()
"end\nend"
);
}
}
TEST_CASE_FIXTURE(ATSFixture, "racing_spawning_1")
{

View file

@ -11,6 +11,8 @@
using namespace Luau;
LUAU_FASTFLAG(LuauAstTypeGroup)
struct JsonEncoderFixture
{
Allocator allocator;
@ -471,11 +473,18 @@ TEST_CASE_FIXTURE(JsonEncoderFixture, "encode_annotation")
{
AstStat* statement = expectParseStatement("type T = ((number) -> (string | nil)) & ((string) -> ())");
if (FFlag::LuauAstTypeGroup)
{
std::string_view expected = R"({"type":"AstStatTypeAlias","location":"0,0 - 0,55","name":"T","generics":[],"genericPacks":[],"value":{"type":"AstTypeIntersection","location":"0,9 - 0,55","types":[{"type":"AstTypeGroup","location":"0,9 - 0,36","type":{"type":"AstTypeFunction","location":"0,10 - 0,36","attributes":[],"generics":[],"genericPacks":[],"argTypes":{"type":"AstTypeList","types":[{"type":"AstTypeReference","location":"0,11 - 0,17","name":"number","nameLocation":"0,11 - 0,17","parameters":[]}]},"argNames":[],"returnTypes":{"type":"AstTypeList","types":[{"type":"AstTypeGroup","location":"0,22 - 0,36","type":{"type":"AstTypeUnion","location":"0,23 - 0,35","types":[{"type":"AstTypeReference","location":"0,23 - 0,29","name":"string","nameLocation":"0,23 - 0,29","parameters":[]},{"type":"AstTypeReference","location":"0,32 - 0,35","name":"nil","nameLocation":"0,32 - 0,35","parameters":[]}]}}]}}},{"type":"AstTypeGroup","location":"0,40 - 0,55","type":{"type":"AstTypeFunction","location":"0,41 - 0,55","attributes":[],"generics":[],"genericPacks":[],"argTypes":{"type":"AstTypeList","types":[{"type":"AstTypeReference","location":"0,42 - 0,48","name":"string","nameLocation":"0,42 - 0,48","parameters":[]}]},"argNames":[],"returnTypes":{"type":"AstTypeList","types":[]}}}]},"exported":false})";
CHECK(toJson(statement) == expected);
}
else
{
std::string_view expected =
R"({"type":"AstStatTypeAlias","location":"0,0 - 0,55","name":"T","generics":[],"genericPacks":[],"value":{"type":"AstTypeIntersection","location":"0,9 - 0,55","types":[{"type":"AstTypeFunction","location":"0,10 - 0,36","attributes":[],"generics":[],"genericPacks":[],"argTypes":{"type":"AstTypeList","types":[{"type":"AstTypeReference","location":"0,11 - 0,17","name":"number","nameLocation":"0,11 - 0,17","parameters":[]}]},"argNames":[],"returnTypes":{"type":"AstTypeList","types":[{"type":"AstTypeUnion","location":"0,23 - 0,35","types":[{"type":"AstTypeReference","location":"0,23 - 0,29","name":"string","nameLocation":"0,23 - 0,29","parameters":[]},{"type":"AstTypeReference","location":"0,32 - 0,35","name":"nil","nameLocation":"0,32 - 0,35","parameters":[]}]}]}},{"type":"AstTypeFunction","location":"0,41 - 0,55","attributes":[],"generics":[],"genericPacks":[],"argTypes":{"type":"AstTypeList","types":[{"type":"AstTypeReference","location":"0,42 - 0,48","name":"string","nameLocation":"0,42 - 0,48","parameters":[]}]},"argNames":[],"returnTypes":{"type":"AstTypeList","types":[]}}]},"exported":false})";
CHECK(toJson(statement) == expected);
}
}
TEST_CASE_FIXTURE(JsonEncoderFixture, "encode_type_literal")
{

View file

@ -4329,4 +4329,21 @@ local var = data;@1
CHECK_EQ(ac.context, AutocompleteContext::Statement);
}
TEST_CASE_FIXTURE(ACBuiltinsFixture, "require_tracing")
{
fileResolver.source["Module/A"] = R"(
return { x = 0 }
)";
fileResolver.source["Module/B"] = R"(
local result = require(script.Parent.A)
local x = 1 + result.
)";
auto ac = autocomplete("Module/B", Position{2, 21});
CHECK(ac.entryMap.size() == 1);
CHECK(ac.entryMap.count("x"));
}
TEST_SUITE_END();

View file

@ -8757,6 +8757,23 @@ end
);
}
TEST_CASE("TypeGroup")
{
CHECK_EQ(
"\n" + compileTypeTable(R"(
function myfunc(test: (string), foo: nil)
end
function myfunc2(test: (string | nil), foo: nil)
end
)"),
R"(
0: function(string, nil)
1: function(string?, nil)
)"
);
}
TEST_CASE("BuiltinFoldMathK")
{
// we can fold math.pi at optimization level 2

View file

@ -3,6 +3,7 @@
#include "Fixture.h"
#include "Luau/EqSatSimplification.h"
#include "Luau/Type.h"
using namespace Luau;
@ -76,7 +77,7 @@ TEST_CASE_FIXTURE(ESFixture, "number | string")
TEST_CASE_FIXTURE(ESFixture, "t1 where t1 = number | t1")
{
TypeId ty = arena->freshType(nullptr);
TypeId ty = arena->freshType(builtinTypes, nullptr);
asMutable(ty)->ty.emplace<UnionType>(std::vector<TypeId>{builtinTypes->numberType, ty});
CHECK("number" == simplifyStr(ty));
@ -450,7 +451,7 @@ TEST_CASE_FIXTURE(ESFixture, "(boolean | nil) & (false | nil)")
TEST_CASE_FIXTURE(ESFixture, "free & string & number")
{
Scope scope{builtinTypes->anyTypePack};
const TypeId freeTy = arena->addType(FreeType{&scope});
const TypeId freeTy = arena->freshType(builtinTypes, &scope);
CHECK("never" == simplifyStr(arena->addType(IntersectionType{{freeTy, builtinTypes->numberType, builtinTypes->stringType}})));
}

View file

@ -9,6 +9,7 @@
#include "Luau/Common.h"
#include "Luau/Frontend.h"
#include "Luau/AutocompleteTypes.h"
#include "Luau/Type.h"
#include <algorithm>
#include <chrono>
@ -27,6 +28,14 @@ LUAU_FASTFLAG(LuauStoreSolverTypeOnModule);
LUAU_FASTFLAG(LexerResumesFromPosition2)
LUAU_FASTFLAG(LuauIncrementalAutocompleteCommentDetection)
LUAU_FASTINT(LuauParseErrorLimit)
LUAU_FASTFLAG(LuauCloneIncrementalModule)
LUAU_FASTFLAG(LuauIncrementalAutocompleteBugfixes)
LUAU_FASTFLAG(LuauReferenceAllocatorInNewSolver)
LUAU_FASTFLAG(LuauMixedModeDefFinderTraversesTypeOf)
LUAU_FASTFLAG(LuauFreeTypesMustHaveBounds)
LUAU_FASTFLAG(LuauBetterReverseDependencyTracking)
static std::optional<AutocompleteEntryMap> nullCallback(std::string tag, std::optional<const ClassType*> ptr, std::optional<std::string> contents)
{
@ -46,15 +55,25 @@ static FrontendOptions getOptions()
return options;
}
static ModuleResolver& getModuleResolver(Luau::Frontend& frontend)
{
return FFlag::LuauSolverV2 ? frontend.moduleResolver : frontend.moduleResolverForAutocomplete;
}
template<class BaseType>
struct FragmentAutocompleteFixtureImpl : BaseType
{
ScopedFastFlag sffs[5] = {
static_assert(std::is_base_of_v<Fixture, BaseType>, "BaseType must be a descendant of Fixture");
ScopedFastFlag sffs[8] = {
{FFlag::LuauAllowFragmentParsing, true},
{FFlag::LuauAutocompleteRefactorsForIncrementalAutocomplete, true},
{FFlag::LuauStoreSolverTypeOnModule, true},
{FFlag::LuauSymbolEquality, true},
{FFlag::LexerResumesFromPosition2, true}
{FFlag::LexerResumesFromPosition2, true},
{FFlag::LuauReferenceAllocatorInNewSolver, true},
{FFlag::LuauIncrementalAutocompleteBugfixes, true},
{FFlag::LuauBetterReverseDependencyTracking, true},
};
FragmentAutocompleteFixtureImpl()
@ -128,6 +147,26 @@ struct FragmentAutocompleteFixtureImpl : BaseType
result = autocompleteFragment(updated, cursorPos, fragmentEndPosition);
assertions(result);
}
std::pair<FragmentTypeCheckStatus, FragmentTypeCheckResult> typecheckFragmentForModule(
const ModuleName& module,
const std::string& document,
Position cursorPos,
std::optional<Position> fragmentEndPosition = std::nullopt
)
{
return Luau::typecheckFragment(this->frontend, module, cursorPos, getOptions(), document, fragmentEndPosition);
}
FragmentAutocompleteResult autocompleteFragmentForModule(
const ModuleName& module,
const std::string& document,
Position cursorPos,
std::optional<Position> fragmentEndPosition = std::nullopt
)
{
return Luau::fragmentAutocomplete(this->frontend, document, module, cursorPos, getOptions(), nullCallback, fragmentEndPosition);
}
};
struct FragmentAutocompleteFixture : FragmentAutocompleteFixtureImpl<Fixture>
@ -162,6 +201,9 @@ end
// 'for autocomplete'.
loadDefinition(fakeVecDecl);
loadDefinition(fakeVecDecl, /* For Autocomplete Module */ true);
addGlobalBinding(frontend.globals, "game", Binding{builtinTypes->anyType});
addGlobalBinding(frontend.globalsForAutocomplete, "game", Binding{builtinTypes->anyType});
}
};
@ -710,6 +752,57 @@ tbl.
CHECK_EQ(AutocompleteContext::Property, fragment.acResults.context);
}
TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "typecheck_fragment_handles_stale_module")
{
const std::string sourceName = "MainModule";
fileResolver.source[sourceName] = "local x = 5";
CheckResult checkResult = frontend.check(sourceName, getOptions());
LUAU_REQUIRE_NO_ERRORS(checkResult);
auto [result, _] = typecheckFragmentForModule(sourceName, fileResolver.source[sourceName], Luau::Position(0, 0));
CHECK_EQ(result, FragmentTypeCheckStatus::Success);
frontend.markDirty(sourceName);
frontend.parse(sourceName);
CHECK_NE(frontend.getSourceModule(sourceName), nullptr);
auto [result2, __] = typecheckFragmentForModule(sourceName, fileResolver.source[sourceName], Luau::Position(0, 0));
CHECK_EQ(result2, FragmentTypeCheckStatus::SkipAutocomplete);
}
TEST_CASE_FIXTURE(FragmentAutocompleteBuiltinsFixture, "typecheck_fragment_handles_unusable_module")
{
const std::string sourceA = "MainModule";
fileResolver.source[sourceA] = R"(
local Modules = game:GetService('Gui').Modules
local B = require(Modules.B)
return { hello = B }
)";
const std::string sourceB = "game/Gui/Modules/B";
fileResolver.source[sourceB] = R"(return {hello = "hello"})";
CheckResult result = frontend.check(sourceA, getOptions());
CHECK(!frontend.isDirty(sourceA, getOptions().forAutocomplete));
std::weak_ptr<Module> weakModule = getModuleResolver(frontend).getModule(sourceB);
REQUIRE(!weakModule.expired());
frontend.markDirty(sourceB);
CHECK(frontend.isDirty(sourceA, getOptions().forAutocomplete));
frontend.check(sourceB, getOptions());
CHECK(weakModule.expired());
auto [status, _] = typecheckFragmentForModule(sourceA, fileResolver.source[sourceA], Luau::Position(0, 0));
CHECK_EQ(status, FragmentTypeCheckStatus::SkipAutocomplete);
auto [status2, _2] = typecheckFragmentForModule(sourceB, fileResolver.source[sourceB], Luau::Position(3, 20));
CHECK_EQ(status2, FragmentTypeCheckStatus::Success);
}
TEST_SUITE_END();
TEST_SUITE_BEGIN("FragmentAutocompleteTests");
@ -1677,4 +1770,187 @@ type A = <>random non code text here
);
}
TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "fragment_autocomplete_handles_stale_module")
{
const std::string sourceName = "MainModule";
fileResolver.source[sourceName] = "local x = 5";
frontend.check(sourceName, getOptions());
frontend.markDirty(sourceName);
frontend.parse(sourceName);
FragmentAutocompleteResult result = autocompleteFragmentForModule(sourceName, fileResolver.source[sourceName], Luau::Position(0, 0));
CHECK(result.acResults.entryMap.empty());
CHECK_EQ(result.incrementalModule, nullptr);
}
TEST_CASE_FIXTURE(FragmentAutocompleteBuiltinsFixture, "require_tracing")
{
fileResolver.source["MainModule/A"] = R"(
return { x = 0 }
)";
fileResolver.source["MainModule"] = R"(
local result = require(script.A)
local x = 1 + result.
)";
autocompleteFragmentInBothSolvers(
fileResolver.source["MainModule"],
fileResolver.source["MainModule"],
Position{2, 21},
[](FragmentAutocompleteResult& result)
{
CHECK(result.acResults.entryMap.size() == 1);
CHECK(result.acResults.entryMap.count("x"));
}
);
}
TEST_CASE_FIXTURE(FragmentAutocompleteBuiltinsFixture, "fragment_ac_must_traverse_typeof_and_not_ice")
{
// This test ensures that we traverse typeof expressions for defs that are being referred to in the fragment
// In this case, we want to ensure we populate the incremental environment with the reference to `m`
// Without this, we would ice as we will refer to the local `m` before it's declaration
ScopedFastFlag sff{FFlag::LuauMixedModeDefFinderTraversesTypeOf, true};
const std::string source = R"(
--!strict
local m = {}
-- and here
function m:m1() end
type nt = typeof(m)
return m
)";
const std::string updated = R"(
--!strict
local m = {}
-- and here
function m:m1() end
type nt = typeof(m)
l
return m
)";
autocompleteFragmentInBothSolvers(source, updated, Position{6, 2}, [](auto& _) {});
}
TEST_CASE_FIXTURE(FragmentAutocompleteBuiltinsFixture, "generalization_crash_when_old_solver_freetypes_have_no_bounds_set")
{
ScopedFastFlag sff{FFlag::LuauFreeTypesMustHaveBounds, true};
const std::string source = R"(
local UserInputService = game:GetService("UserInputService");
local Camera = workspace.CurrentCamera;
UserInputService.InputBegan:Connect(function(Input)
if (Input.KeyCode == Enum.KeyCode.One) then
local Up = Input.Foo
local Vector = -(Up:Unit)
end
end)
)";
const std::string dest = R"(
local UserInputService = game:GetService("UserInputService");
local Camera = workspace.CurrentCamera;
UserInputService.InputBegan:Connect(function(Input)
if (Input.KeyCode == Enum.KeyCode.One) then
local Up = Input.Foo
local Vector = -(Up:Unit())
end
end)
)";
autocompleteFragmentInBothSolvers(source, dest, Position{8, 36}, [](auto& _) {});
}
TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "fragment_autocomplete_ensures_memory_isolation")
{
ScopedFastFlag sff{FFlag::LuauCloneIncrementalModule, true};
ToStringOptions opt;
opt.exhaustive = true;
opt.exhaustive = true;
opt.functionTypeArguments = true;
opt.maxTableLength = 0;
opt.maxTypeLength = 0;
auto checkAndExamine = [&](const std::string& src, const std::string& idName, const std::string& idString)
{
check(src, getOptions());
auto id = getType(idName, true);
LUAU_ASSERT(id);
CHECK_EQ(Luau::toString(*id, opt), idString);
};
auto getTypeFromModule = [](ModulePtr module, const std::string& name) -> std::optional<TypeId>
{
if (!module->hasModuleScope())
return std::nullopt;
return lookupName(module->getModuleScope(), name);
};
auto fragmentACAndCheck = [&](const std::string& updated, const Position& pos, const std::string& idName)
{
FragmentAutocompleteResult result = autocompleteFragment(updated, pos, std::nullopt);
auto fragId = getTypeFromModule(result.incrementalModule, idName);
LUAU_ASSERT(fragId);
auto srcId = getType(idName, true);
LUAU_ASSERT(srcId);
CHECK((*fragId)->owningArena != (*srcId)->owningArena);
CHECK(&(result.incrementalModule->internalTypes) == (*fragId)->owningArena);
};
const std::string source = R"(local module = {}
f
return module)";
const std::string updated1 = R"(local module = {}
function module.a
return module)";
const std::string updated2 = R"(local module = {}
function module.ab
return module)";
{
ScopedFastFlag sff{FFlag::LuauSolverV2, false};
checkAndExamine(source, "module", "{ }");
// [TODO] CLI-140762 we shouldn't mutate stale module in autocompleteFragment
// early return since the following checking will fail, which it shouldn't!
fragmentACAndCheck(updated1, Position{1, 17}, "module");
fragmentACAndCheck(updated2, Position{1, 18}, "module");
}
{
ScopedFastFlag sff{FFlag::LuauSolverV2, true};
checkAndExamine(source, "module", "{ }");
// [TODO] CLI-140762 we shouldn't mutate stale module in autocompleteFragment
// early return since the following checking will fail, which it shouldn't!
fragmentACAndCheck(updated1, Position{1, 17}, "module");
fragmentACAndCheck(updated2, Position{1, 18}, "module");
}
}
TEST_CASE_FIXTURE(FragmentAutocompleteFixture, "fragment_autocomplete_shouldnt_crash_on_cross_module_mutation")
{
ScopedFastFlag sff{FFlag::LuauCloneIncrementalModule, true};
const std::string source = R"(local module = {}
function module.
return module
)";
const std::string updated = R"(local module = {}
function module.f
return module
)";
autocompleteFragmentInBothSolvers(source, updated, Position{1, 18}, [](FragmentAutocompleteResult& result) {});
}
TEST_SUITE_END();

View file

@ -1,6 +1,7 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#include "Luau/AstQuery.h"
#include "Luau/BuiltinDefinitions.h"
#include "Luau/DenseHash.h"
#include "Luau/Frontend.h"
#include "Luau/RequireTracer.h"
@ -17,6 +18,7 @@ LUAU_FASTFLAG(DebugLuauFreezeArena);
LUAU_FASTFLAG(DebugLuauMagicTypes);
LUAU_FASTFLAG(LuauReferenceAllocatorInNewSolver);
LUAU_FASTFLAG(LuauSelectivelyRetainDFGArena)
LUAU_FASTFLAG(LuauBetterReverseDependencyTracking);
namespace
{
@ -1572,4 +1574,207 @@ return {x = a, y = b, z = c}
CHECK(mod->keyArena.allocator.empty());
}
TEST_CASE_FIXTURE(FrontendFixture, "test_traverse_dependents")
{
ScopedFastFlag dependencyTracking{FFlag::LuauBetterReverseDependencyTracking, true};
fileResolver.source["game/Gui/Modules/A"] = "return {hello=5, world=true}";
fileResolver.source["game/Gui/Modules/B"] = R"(
return require(game:GetService('Gui').Modules.A)
)";
fileResolver.source["game/Gui/Modules/C"] = R"(
local Modules = game:GetService('Gui').Modules
local B = require(Modules.B)
return {c_value = B.hello}
)";
fileResolver.source["game/Gui/Modules/D"] = R"(
local Modules = game:GetService('Gui').Modules
local C = require(Modules.C)
return {d_value = C.c_value}
)";
frontend.check("game/Gui/Modules/D");
std::vector<ModuleName> visited;
frontend.traverseDependents(
"game/Gui/Modules/B",
[&visited](SourceNode& node)
{
visited.push_back(node.name);
return true;
}
);
CHECK_EQ(std::vector<ModuleName>{"game/Gui/Modules/B", "game/Gui/Modules/C", "game/Gui/Modules/D"}, visited);
}
TEST_CASE_FIXTURE(FrontendFixture, "test_traverse_dependents_early_exit")
{
ScopedFastFlag dependencyTracking{FFlag::LuauBetterReverseDependencyTracking, true};
fileResolver.source["game/Gui/Modules/A"] = "return {hello=5, world=true}";
fileResolver.source["game/Gui/Modules/B"] = R"(
return require(game:GetService('Gui').Modules.A)
)";
fileResolver.source["game/Gui/Modules/C"] = R"(
local Modules = game:GetService('Gui').Modules
local B = require(Modules.B)
return {c_value = B.hello}
)";
frontend.check("game/Gui/Modules/C");
std::vector<ModuleName> visited;
frontend.traverseDependents(
"game/Gui/Modules/A",
[&visited](SourceNode& node)
{
visited.push_back(node.name);
return node.name != "game/Gui/Modules/B";
}
);
CHECK_EQ(std::vector<ModuleName>{"game/Gui/Modules/A", "game/Gui/Modules/B"}, visited);
}
TEST_CASE_FIXTURE(FrontendFixture, "test_dependents_stored_on_node_as_graph_updates")
{
ScopedFastFlag dependencyTracking{FFlag::LuauBetterReverseDependencyTracking, true};
auto updateSource = [&](const std::string& name, const std::string& source)
{
fileResolver.source[name] = source;
frontend.markDirty(name);
};
auto validateMatchesRequireLists = [&](const std::string& message)
{
DenseHashMap<ModuleName, std::vector<ModuleName>> dependents{{}};
for (const auto& module : frontend.sourceNodes)
{
for (const auto& dep : module.second->requireSet)
dependents[dep].push_back(module.first);
}
for (const auto& module : frontend.sourceNodes)
{
Set<ModuleName>& dependentsForModule = module.second->dependents;
for (const auto& dep : dependents[module.first])
CHECK_MESSAGE(1 == dependentsForModule.count(dep), "Mismatch in dependents for " << module.first << ": " << message);
}
};
auto validateSecondDependsOnFirst = [&](const std::string& from, const std::string& to, bool expected)
{
SourceNode& fromNode = *frontend.sourceNodes[from];
CHECK_MESSAGE(
fromNode.dependents.count(to) == int(expected),
"Expected " << from << " to " << (expected ? std::string() : std::string("not ")) << "have a reverse dependency on " << to
);
};
// C -> B -> A
{
updateSource("game/Gui/Modules/A", "return {hello=5, world=true}");
updateSource("game/Gui/Modules/B", R"(
return require(game:GetService('Gui').Modules.A)
)");
updateSource("game/Gui/Modules/C", R"(
local Modules = game:GetService('Gui').Modules
local B = require(Modules.B)
return {c_value = B}
)");
frontend.check("game/Gui/Modules/C");
validateMatchesRequireLists("Initial check");
validateSecondDependsOnFirst("game/Gui/Modules/A", "game/Gui/Modules/B", true);
validateSecondDependsOnFirst("game/Gui/Modules/B", "game/Gui/Modules/C", true);
validateSecondDependsOnFirst("game/Gui/Modules/C", "game/Gui/Modules/A", false);
}
// C -> B, A
{
updateSource("game/Gui/Modules/B", R"(
return 1
)");
frontend.check("game/Gui/Modules/C");
validateMatchesRequireLists("Removing dependency B->A");
validateSecondDependsOnFirst("game/Gui/Modules/A", "game/Gui/Modules/B", false);
}
// C -> B -> A
{
updateSource("game/Gui/Modules/B", R"(
return require(game:GetService('Gui').Modules.A)
)");
frontend.check("game/Gui/Modules/C");
validateMatchesRequireLists("Adding back B->A");
validateSecondDependsOnFirst("game/Gui/Modules/A", "game/Gui/Modules/B", true);
}
// C -> B -> A, D -> (C,B,A)
{
updateSource("game/Gui/Modules/D", R"(
local C = require(game:GetService('Gui').Modules.C)
local B = require(game:GetService('Gui').Modules.B)
local A = require(game:GetService('Gui').Modules.A)
return {d_value = C.c_value}
)");
frontend.check("game/Gui/Modules/D");
validateMatchesRequireLists("Adding D->C, D->B, D->A");
validateSecondDependsOnFirst("game/Gui/Modules/A", "game/Gui/Modules/D", true);
validateSecondDependsOnFirst("game/Gui/Modules/B", "game/Gui/Modules/D", true);
validateSecondDependsOnFirst("game/Gui/Modules/C", "game/Gui/Modules/D", true);
}
// B -> A, C <-> D
{
updateSource("game/Gui/Modules/D", "return require(game:GetService('Gui').Modules.C)");
updateSource("game/Gui/Modules/C", "return require(game:GetService('Gui').Modules.D)");
frontend.check("game/Gui/Modules/D");
validateMatchesRequireLists("Adding cycle D->C, C->D");
validateSecondDependsOnFirst("game/Gui/Modules/C", "game/Gui/Modules/D", true);
validateSecondDependsOnFirst("game/Gui/Modules/D", "game/Gui/Modules/C", true);
}
// B -> A, C -> D, D -> error
{
updateSource("game/Gui/Modules/D", "return require(game:GetService('Gui').Modules.C.)");
frontend.check("game/Gui/Modules/D");
validateMatchesRequireLists("Adding error dependency D->C.");
validateSecondDependsOnFirst("game/Gui/Modules/D", "game/Gui/Modules/C", true);
validateSecondDependsOnFirst("game/Gui/Modules/C", "game/Gui/Modules/D", false);
}
}
TEST_CASE_FIXTURE(FrontendFixture, "test_invalid_dependency_tracking_per_module_resolver")
{
ScopedFastFlag dependencyTracking{FFlag::LuauBetterReverseDependencyTracking, true};
ScopedFastFlag newSolver{FFlag::LuauSolverV2, false};
fileResolver.source["game/Gui/Modules/A"] = "return {hello=5, world=true}";
fileResolver.source["game/Gui/Modules/B"] = "return require(game:GetService('Gui').Modules.A)";
FrontendOptions opts;
opts.forAutocomplete = false;
frontend.check("game/Gui/Modules/B", opts);
CHECK(frontend.allModuleDependenciesValid("game/Gui/Modules/B", opts.forAutocomplete));
CHECK(!frontend.allModuleDependenciesValid("game/Gui/Modules/B", !opts.forAutocomplete));
opts.forAutocomplete = true;
frontend.check("game/Gui/Modules/A", opts);
CHECK(!frontend.allModuleDependenciesValid("game/Gui/Modules/B", opts.forAutocomplete));
CHECK(frontend.allModuleDependenciesValid("game/Gui/Modules/B", !opts.forAutocomplete));
CHECK(frontend.allModuleDependenciesValid("game/Gui/Modules/A", !opts.forAutocomplete));
CHECK(frontend.allModuleDependenciesValid("game/Gui/Modules/A", opts.forAutocomplete));
}
TEST_SUITE_END();

View file

@ -179,9 +179,9 @@ TEST_CASE_FIXTURE(GeneralizationFixture, "functions_containing_cyclic_tables_can
TEST_CASE_FIXTURE(GeneralizationFixture, "union_type_traversal_doesnt_crash")
{
// t1 where t1 = ('h <: (t1 <: 'i)) | ('j <: (t1 <: 'i))
TypeId i = arena.addType(FreeType{NotNull{globalScope.get()}});
TypeId h = arena.addType(FreeType{NotNull{globalScope.get()}});
TypeId j = arena.addType(FreeType{NotNull{globalScope.get()}});
TypeId i = arena.freshType(NotNull{&builtinTypes}, globalScope.get());
TypeId h = arena.freshType(NotNull{&builtinTypes}, globalScope.get());
TypeId j = arena.freshType(NotNull{&builtinTypes}, globalScope.get());
TypeId unionType = arena.addType(UnionType{{h, j}});
getMutable<FreeType>(h)->upperBound = i;
getMutable<FreeType>(h)->lowerBound = builtinTypes.neverType;
@ -196,9 +196,9 @@ TEST_CASE_FIXTURE(GeneralizationFixture, "union_type_traversal_doesnt_crash")
TEST_CASE_FIXTURE(GeneralizationFixture, "intersection_type_traversal_doesnt_crash")
{
// t1 where t1 = ('h <: (t1 <: 'i)) & ('j <: (t1 <: 'i))
TypeId i = arena.addType(FreeType{NotNull{globalScope.get()}});
TypeId h = arena.addType(FreeType{NotNull{globalScope.get()}});
TypeId j = arena.addType(FreeType{NotNull{globalScope.get()}});
TypeId i = arena.freshType(NotNull{&builtinTypes}, globalScope.get());
TypeId h = arena.freshType(NotNull{&builtinTypes}, globalScope.get());
TypeId j = arena.freshType(NotNull{&builtinTypes}, globalScope.get());
TypeId intersectionType = arena.addType(IntersectionType{{h, j}});
getMutable<FreeType>(h)->upperBound = i;

View file

@ -4,6 +4,7 @@
#include "Fixture.h"
#include "ClassFixture.h"
#include "Luau/Type.h"
#include "ScopedFlags.h"
#include "doctest.h"
@ -29,7 +30,7 @@ TEST_CASE_FIXTURE(Fixture, "weird_cyclic_instantiation")
DenseHashMap<TypeId, TypeId> genericSubstitutions{nullptr};
DenseHashMap<TypePackId, TypePackId> genericPackSubstitutions{nullptr};
TypeId freeTy = arena.freshType(&scope);
TypeId freeTy = arena.freshType(builtinTypes, &scope);
FreeType* ft = getMutable<FreeType>(freeTy);
REQUIRE(ft);
ft->lowerBound = idTy;

View file

@ -248,4 +248,185 @@ TEST_CASE("string_interpolation_with_unicode_escape")
CHECK_EQ(lexer.next().type, Lexeme::Eof);
}
TEST_CASE("single_quoted_string")
{
const std::string testInput = "'test'";
Luau::Allocator alloc;
AstNameTable table(alloc);
Lexer lexer(testInput.c_str(), testInput.size(), table);
Lexeme lexeme = lexer.next();
CHECK_EQ(lexeme.type, Lexeme::QuotedString);
CHECK_EQ(lexeme.getQuoteStyle(), Lexeme::QuoteStyle::Single);
}
TEST_CASE("double_quoted_string")
{
const std::string testInput = R"("test")";
Luau::Allocator alloc;
AstNameTable table(alloc);
Lexer lexer(testInput.c_str(), testInput.size(), table);
Lexeme lexeme = lexer.next();
CHECK_EQ(lexeme.type, Lexeme::QuotedString);
CHECK_EQ(lexeme.getQuoteStyle(), Lexeme::QuoteStyle::Double);
}
TEST_CASE("lexer_determines_string_block_depth_0")
{
const std::string testInput = "[[ test ]]";
Luau::Allocator alloc;
AstNameTable table(alloc);
Lexer lexer(testInput.c_str(), testInput.size(), table);
Lexeme lexeme = lexer.next();
REQUIRE_EQ(lexeme.type, Lexeme::RawString);
CHECK_EQ(lexeme.getBlockDepth(), 0);
}
TEST_CASE("lexer_determines_string_block_depth_0_multiline_1")
{
const std::string testInput = R"([[ test
]])";
Luau::Allocator alloc;
AstNameTable table(alloc);
Lexer lexer(testInput.c_str(), testInput.size(), table);
Lexeme lexeme = lexer.next();
REQUIRE_EQ(lexeme.type, Lexeme::RawString);
CHECK_EQ(lexeme.getBlockDepth(), 0);
}
TEST_CASE("lexer_determines_string_block_depth_0_multiline_2")
{
const std::string testInput = R"([[
test
]])";
Luau::Allocator alloc;
AstNameTable table(alloc);
Lexer lexer(testInput.c_str(), testInput.size(), table);
Lexeme lexeme = lexer.next();
REQUIRE_EQ(lexeme.type, Lexeme::RawString);
CHECK_EQ(lexeme.getBlockDepth(), 0);
}
TEST_CASE("lexer_determines_string_block_depth_0_multiline_3")
{
const std::string testInput = R"([[
test ]])";
Luau::Allocator alloc;
AstNameTable table(alloc);
Lexer lexer(testInput.c_str(), testInput.size(), table);
Lexeme lexeme = lexer.next();
REQUIRE_EQ(lexeme.type, Lexeme::RawString);
CHECK_EQ(lexeme.getBlockDepth(), 0);
}
TEST_CASE("lexer_determines_string_block_depth_1")
{
const std::string testInput = "[=[[%s]]=]";
Luau::Allocator alloc;
AstNameTable table(alloc);
Lexer lexer(testInput.c_str(), testInput.size(), table);
Lexeme lexeme = lexer.next();
REQUIRE_EQ(lexeme.type, Lexeme::RawString);
CHECK_EQ(lexeme.getBlockDepth(), 1);
}
TEST_CASE("lexer_determines_string_block_depth_2")
{
const std::string testInput = "[==[ test ]==]";
Luau::Allocator alloc;
AstNameTable table(alloc);
Lexer lexer(testInput.c_str(), testInput.size(), table);
Lexeme lexeme = lexer.next();
REQUIRE_EQ(lexeme.type, Lexeme::RawString);
CHECK_EQ(lexeme.getBlockDepth(), 2);
}
TEST_CASE("lexer_determines_string_block_depth_2_multiline_1")
{
const std::string testInput = R"([==[ test
]==])";
Luau::Allocator alloc;
AstNameTable table(alloc);
Lexer lexer(testInput.c_str(), testInput.size(), table);
Lexeme lexeme = lexer.next();
REQUIRE_EQ(lexeme.type, Lexeme::RawString);
CHECK_EQ(lexeme.getBlockDepth(), 2);
}
TEST_CASE("lexer_determines_string_block_depth_2_multiline_2")
{
const std::string testInput = R"([==[
test
]==])";
Luau::Allocator alloc;
AstNameTable table(alloc);
Lexer lexer(testInput.c_str(), testInput.size(), table);
Lexeme lexeme = lexer.next();
REQUIRE_EQ(lexeme.type, Lexeme::RawString);
CHECK_EQ(lexeme.getBlockDepth(), 2);
}
TEST_CASE("lexer_determines_string_block_depth_2_multiline_3")
{
const std::string testInput = R"([==[
test ]==])";
Luau::Allocator alloc;
AstNameTable table(alloc);
Lexer lexer(testInput.c_str(), testInput.size(), table);
Lexeme lexeme = lexer.next();
REQUIRE_EQ(lexeme.type, Lexeme::RawString);
CHECK_EQ(lexeme.getBlockDepth(), 2);
}
TEST_CASE("lexer_determines_comment_block_depth_0")
{
const std::string testInput = "--[[ test ]]";
Luau::Allocator alloc;
AstNameTable table(alloc);
Lexer lexer(testInput.c_str(), testInput.size(), table);
Lexeme lexeme = lexer.next();
REQUIRE_EQ(lexeme.type, Lexeme::BlockComment);
CHECK_EQ(lexeme.getBlockDepth(), 0);
}
TEST_CASE("lexer_determines_string_block_depth_1")
{
const std::string testInput = "--[=[ μέλλον ]=]";
Luau::Allocator alloc;
AstNameTable table(alloc);
Lexer lexer(testInput.c_str(), testInput.size(), table);
Lexeme lexeme = lexer.next();
REQUIRE_EQ(lexeme.type, Lexeme::BlockComment);
CHECK_EQ(lexeme.getBlockDepth(), 1);
}
TEST_CASE("lexer_determines_string_block_depth_2")
{
const std::string testInput = "--[==[ test ]==]";
Luau::Allocator alloc;
AstNameTable table(alloc);
Lexer lexer(testInput.c_str(), testInput.size(), table);
Lexeme lexeme = lexer.next();
REQUIRE_EQ(lexeme.type, Lexeme::BlockComment);
CHECK_EQ(lexeme.getBlockDepth(), 2);
}
TEST_SUITE_END();

View file

@ -12,7 +12,7 @@
LUAU_FASTFLAG(LuauSolverV2)
LUAU_FASTINT(LuauTypeInferRecursionLimit)
LUAU_FASTFLAG(LuauNormalizationTracksCyclicPairsThroughInhabitance)
LUAU_FASTFLAG(LuauFixNormalizedIntersectionOfNegatedClass)
using namespace Luau;
namespace
@ -851,17 +851,17 @@ TEST_CASE_FIXTURE(NormalizeFixture, "crazy_metatable")
TEST_CASE_FIXTURE(NormalizeFixture, "negations_of_classes")
{
ScopedFastFlag _{FFlag::LuauFixNormalizedIntersectionOfNegatedClass, true};
createSomeClasses(&frontend);
CHECK("(Parent & ~Child) | Unrelated" == toString(normal("(Parent & Not<Child>) | Unrelated")));
CHECK("((class & ~Child) | boolean | buffer | function | number | string | table | thread)?" == toString(normal("Not<Child>")));
CHECK("Child" == toString(normal("Not<Parent> & Child")));
CHECK("never" == toString(normal("Not<Parent> & Child")));
CHECK("((class & ~Parent) | Child | boolean | buffer | function | number | string | table | thread)?" == toString(normal("Not<Parent> | Child")));
CHECK("(boolean | buffer | function | number | string | table | thread)?" == toString(normal("Not<cls>")));
CHECK(
"(Parent | Unrelated | boolean | buffer | function | number | string | table | thread)?" ==
toString(normal("Not<cls & Not<Parent> & Not<Child> & Not<Unrelated>>"))
);
CHECK("Child" == toString(normal("(Child | Unrelated) & Not<Unrelated>")));
}
@ -962,7 +962,7 @@ TEST_CASE_FIXTURE(NormalizeFixture, "final_types_are_cached")
TEST_CASE_FIXTURE(NormalizeFixture, "non_final_types_can_be_normalized_but_are_not_cached")
{
TypeId a = arena.freshType(&globalScope);
TypeId a = arena.freshType(builtinTypes, &globalScope);
std::shared_ptr<const NormalizedType> na1 = normalizer.normalize(a);
std::shared_ptr<const NormalizedType> na2 = normalizer.normalize(a);
@ -1034,7 +1034,7 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "normalizer_should_be_able_to_detect_cyclic_t
if (!FFlag::LuauSolverV2)
return;
ScopedFastInt sfi{FInt::LuauTypeInferRecursionLimit, 0};
ScopedFastFlag sff{FFlag::LuauNormalizationTracksCyclicPairsThroughInhabitance, true};
CheckResult result = check(R"(
--!strict

View file

@ -21,6 +21,8 @@ LUAU_FASTFLAG(LuauErrorRecoveryForTableTypes)
LUAU_FASTFLAG(LuauErrorRecoveryForClassNames)
LUAU_FASTFLAG(LuauFixFunctionNameStartPosition)
LUAU_FASTFLAG(LuauExtendStatEndPosWithSemicolon)
LUAU_FASTFLAG(LuauPreserveUnionIntersectionNodeForLeadingTokenSingleType)
LUAU_FASTFLAG(LuauAstTypeGroup)
namespace
{
@ -369,6 +371,9 @@ TEST_CASE_FIXTURE(Fixture, "return_type_is_an_intersection_type_if_led_with_one_
AstTypeIntersection* returnAnnotation = annotation->returnTypes.types.data[0]->as<AstTypeIntersection>();
REQUIRE(returnAnnotation != nullptr);
if (FFlag::LuauAstTypeGroup)
CHECK(returnAnnotation->types.data[0]->as<AstTypeGroup>());
else
CHECK(returnAnnotation->types.data[0]->as<AstTypeReference>());
CHECK(returnAnnotation->types.data[1]->as<AstTypeFunction>());
}
@ -2418,6 +2423,91 @@ TEST_CASE_FIXTURE(Fixture, "invalid_user_defined_type_functions")
matchParseError("type function foo() local v1 = 1; type function bar() print(v1) end end", "Type function cannot reference outer local 'v1'");
}
TEST_CASE_FIXTURE(Fixture, "leading_union_intersection_with_single_type_preserves_the_union_intersection_ast_node")
{
ScopedFastFlag _{FFlag::LuauPreserveUnionIntersectionNodeForLeadingTokenSingleType, true};
AstStatBlock* block = parse(R"(
type Foo = | string
type Bar = & number
)");
REQUIRE_EQ(2, block->body.size);
const auto alias1 = block->body.data[0]->as<AstStatTypeAlias>();
REQUIRE(alias1);
const auto unionType = alias1->type->as<AstTypeUnion>();
REQUIRE(unionType);
CHECK_EQ(1, unionType->types.size);
const auto alias2 = block->body.data[1]->as<AstStatTypeAlias>();
REQUIRE(alias2);
const auto intersectionType = alias2->type->as<AstTypeIntersection>();
REQUIRE(intersectionType);
CHECK_EQ(1, intersectionType->types.size);
}
TEST_CASE_FIXTURE(Fixture, "parse_simple_ast_type_group")
{
ScopedFastFlag _{FFlag::LuauAstTypeGroup, true};
AstStatBlock* stat = parse(R"(
type Foo = (string)
)");
REQUIRE(stat);
REQUIRE_EQ(1, stat->body.size);
auto alias1 = stat->body.data[0]->as<AstStatTypeAlias>();
REQUIRE(alias1);
auto group1 = alias1->type->as<AstTypeGroup>();
REQUIRE(group1);
CHECK(group1->type->is<AstTypeReference>());
}
TEST_CASE_FIXTURE(Fixture, "parse_nested_ast_type_group")
{
ScopedFastFlag _{FFlag::LuauAstTypeGroup, true};
AstStatBlock* stat = parse(R"(
type Foo = ((string))
)");
REQUIRE(stat);
REQUIRE_EQ(1, stat->body.size);
auto alias1 = stat->body.data[0]->as<AstStatTypeAlias>();
REQUIRE(alias1);
auto group1 = alias1->type->as<AstTypeGroup>();
REQUIRE(group1);
auto group2 = group1->type->as<AstTypeGroup>();
REQUIRE(group2);
CHECK(group2->type->is<AstTypeReference>());
}
TEST_CASE_FIXTURE(Fixture, "parse_return_type_ast_type_group")
{
ScopedFastFlag _{FFlag::LuauAstTypeGroup, true};
AstStatBlock* stat = parse(R"(
type Foo = () -> (string)
)");
REQUIRE(stat);
REQUIRE_EQ(1, stat->body.size);
auto alias1 = stat->body.data[0]->as<AstStatTypeAlias>();
REQUIRE(alias1);
auto funcType = alias1->type->as<AstTypeFunction>();
REQUIRE(funcType);
REQUIRE_EQ(1, funcType->returnTypes.types.size);
REQUIRE(!funcType->returnTypes.tailType);
CHECK(funcType->returnTypes.types.data[0]->is<AstTypeGroup>());
}
TEST_SUITE_END();
TEST_SUITE_BEGIN("ParseErrorRecovery");
@ -3688,6 +3778,13 @@ TEST_CASE_FIXTURE(Fixture, "grouped_function_type")
auto unionTy = paramTy.type->as<AstTypeUnion>();
LUAU_ASSERT(unionTy);
CHECK_EQ(unionTy->types.size, 2);
if (FFlag::LuauAstTypeGroup)
{
auto groupTy = unionTy->types.data[0]->as<AstTypeGroup>(); // (() -> ())
REQUIRE(groupTy);
CHECK(groupTy->type->is<AstTypeFunction>()); // () -> ()
}
else
CHECK(unionTy->types.data[0]->is<AstTypeFunction>()); // () -> ()
CHECK(unionTy->types.data[1]->is<AstTypeReference>()); // nil
}

View file

@ -324,8 +324,7 @@ TEST_CASE_FIXTURE(Fixture, "free")
{
DOES_NOT_PASS_NEW_SOLVER_GUARD();
Type type{TypeVariant{FreeType{TypeLevel{0, 0}}}};
Type type{TypeVariant{FreeType{TypeLevel{0, 0}, builtinTypes->neverType, builtinTypes->unknownType}}};
ToDotOptions opts;
opts.showPointers = false;
CHECK_EQ(

File diff suppressed because it is too large Load diff

View file

@ -14,6 +14,7 @@ using namespace Luau;
LUAU_FASTFLAG(LuauSolverV2)
LUAU_DYNAMIC_FASTINT(LuauTypeFamilyApplicationCartesianProductLimit)
LUAU_FASTFLAG(LuauMetatableTypeFunctions)
struct TypeFunctionFixture : Fixture
{
@ -1262,4 +1263,195 @@ TEST_CASE_FIXTURE(Fixture, "fuzz_len_type_function_follow")
)");
}
TEST_CASE_FIXTURE(BuiltinsFixture, "setmetatable_type_function_assigns_correct_metatable")
{
if (!FFlag::LuauSolverV2 || !FFlag::LuauMetatableTypeFunctions)
return;
CheckResult result = check(R"(
type Identity = setmetatable<{}, { __index: {} }>
)");
LUAU_REQUIRE_NO_ERRORS(result);
TypeId id = requireTypeAlias("Identity");
CHECK_EQ(toString(id, {true}), "{ @metatable { __index: { } }, { } }");
const MetatableType* mt = get<MetatableType>(id);
REQUIRE(mt);
CHECK_EQ(toString(mt->metatable), "{ __index: { } }");
}
TEST_CASE_FIXTURE(BuiltinsFixture, "setmetatable_type_function_assigns_correct_metatable_2")
{
if (!FFlag::LuauSolverV2 || !FFlag::LuauMetatableTypeFunctions)
return;
CheckResult result = check(R"(
type Identity = setmetatable<{}, { __index: {} }>
type FooBar = setmetatable<{}, Identity>
)");
LUAU_REQUIRE_NO_ERRORS(result);
TypeId id = requireTypeAlias("Identity");
CHECK_EQ(toString(id, {true}), "{ @metatable { __index: { } }, { } }");
const MetatableType* mt = get<MetatableType>(id);
REQUIRE(mt);
CHECK_EQ(toString(mt->metatable), "{ __index: { } }");
TypeId foobar = requireTypeAlias("FooBar");
const MetatableType* mt2 = get<MetatableType>(foobar);
REQUIRE(mt2);
CHECK_EQ(toString(mt2->metatable, {true}), "{ @metatable { __index: { } }, { } }");
}
TEST_CASE_FIXTURE(BuiltinsFixture, "setmetatable_type_function_errors_on_metatable_with_metatable_metamethod")
{
if (!FFlag::LuauSolverV2 || !FFlag::LuauMetatableTypeFunctions)
return;
CheckResult result = check(R"(
type Identity = setmetatable<{}, { __metatable: "blocked" }>
type Bad = setmetatable<Identity, { __index: {} }>
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
TypeId id = requireTypeAlias("Identity");
CHECK_EQ(toString(id, {true}), "{ @metatable { __metatable: \"blocked\" }, { } }");
const MetatableType* mt = get<MetatableType>(id);
REQUIRE(mt);
CHECK_EQ(toString(mt->metatable), "{ __metatable: \"blocked\" }");
}
TEST_CASE_FIXTURE(BuiltinsFixture, "setmetatable_type_function_errors_on_invalid_set")
{
if (!FFlag::LuauSolverV2 || !FFlag::LuauMetatableTypeFunctions)
return;
CheckResult result = check(R"(
type Identity = setmetatable<string, {}>
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
}
TEST_CASE_FIXTURE(BuiltinsFixture, "setmetatable_type_function_errors_on_nontable_metatable")
{
if (!FFlag::LuauSolverV2 || !FFlag::LuauMetatableTypeFunctions)
return;
CheckResult result = check(R"(
type Identity = setmetatable<{}, string>
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
}
TEST_CASE_FIXTURE(BuiltinsFixture, "getmetatable_type_function_returns_nil_if_no_metatable")
{
if (!FFlag::LuauSolverV2 || !FFlag::LuauMetatableTypeFunctions)
return;
CheckResult result = check(R"(
type TableWithNoMetatable = getmetatable<{}>
type NumberWithNoMetatable = getmetatable<number>
type BooleanWithNoMetatable = getmetatable<boolean>
type BooleanLiteralWithNoMetatable = getmetatable<true>
)");
LUAU_REQUIRE_NO_ERRORS(result);
auto tableResult = requireTypeAlias("TableWithNoMetatable");
CHECK_EQ(toString(tableResult), "nil");
auto numberResult = requireTypeAlias("NumberWithNoMetatable");
CHECK_EQ(toString(numberResult), "nil");
auto booleanResult = requireTypeAlias("BooleanWithNoMetatable");
CHECK_EQ(toString(booleanResult), "nil");
auto booleanLiteralResult = requireTypeAlias("BooleanLiteralWithNoMetatable");
CHECK_EQ(toString(booleanLiteralResult), "nil");
}
TEST_CASE_FIXTURE(BuiltinsFixture, "getmetatable_returns_correct_metatable")
{
if (!FFlag::LuauSolverV2 || !FFlag::LuauMetatableTypeFunctions)
return;
CheckResult result = check(R"(
local metatable = { __index = { w = 4 } }
local obj = setmetatable({x = 1, y = 2, z = 3}, metatable)
type Metatable = getmetatable<typeof(obj)>
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ(toString(requireTypeAlias("Metatable"), {true}), "{ __index: { w: number } }");
}
TEST_CASE_FIXTURE(BuiltinsFixture, "getmetatable_returns_correct_metatable_for_union")
{
if (!FFlag::LuauSolverV2 || !FFlag::LuauMetatableTypeFunctions)
return;
CheckResult result = check(R"(
type Identity = setmetatable<{}, {}>
type Metatable = getmetatable<string | Identity>
type IntersectMetatable = getmetatable<string & Identity>
)");
LUAU_REQUIRE_NO_ERRORS(result);
const PrimitiveType* stringType = get<PrimitiveType>(builtinTypes->stringType);
REQUIRE(stringType->metatable);
TypeArena arena = TypeArena{};
std::string expected1 = toString(arena.addType(UnionType{{*stringType->metatable, builtinTypes->emptyTableType}}), {true});
CHECK_EQ(toString(requireTypeAlias("Metatable"), {true}), expected1);
std::string expected2 = toString(arena.addType(IntersectionType{{*stringType->metatable, builtinTypes->emptyTableType}}), {true});
CHECK_EQ(toString(requireTypeAlias("IntersectMetatable"), {true}), expected2);
}
TEST_CASE_FIXTURE(BuiltinsFixture, "getmetatable_returns_correct_metatable_for_string")
{
if (!FFlag::LuauSolverV2 || !FFlag::LuauMetatableTypeFunctions)
return;
CheckResult result = check(R"(
type Metatable = getmetatable<string>
type Metatable2 = getmetatable<"foo">
)");
LUAU_REQUIRE_NO_ERRORS(result);
const PrimitiveType* stringType = get<PrimitiveType>(builtinTypes->stringType);
REQUIRE(stringType->metatable);
std::string expected = toString(*stringType->metatable, {true});
CHECK_EQ(toString(requireTypeAlias("Metatable"), {true}), expected);
CHECK_EQ(toString(requireTypeAlias("Metatable2"), {true}), expected);
}
TEST_CASE_FIXTURE(BuiltinsFixture, "getmetatable_respects_metatable_metamethod")
{
if (!FFlag::LuauSolverV2 || !FFlag::LuauMetatableTypeFunctions)
return;
CheckResult result = check(R"(
local metatable = { __metatable = "Test" }
local obj = setmetatable({x = 1, y = 2, z = 3}, metatable)
type Metatable = getmetatable<typeof(obj)>
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ(toString(requireTypeAlias("Metatable")), "string");
}
TEST_SUITE_END();

View file

@ -8,7 +8,6 @@
using namespace Luau;
LUAU_FASTFLAG(LuauSolverV2)
LUAU_FASTFLAG(LuauUserTypeFunFixNoReadWrite)
LUAU_FASTFLAG(LuauUserTypeFunFixInner)
LUAU_FASTFLAG(LuauUserTypeFunGenerics)
LUAU_FASTFLAG(LuauUserTypeFunCloneTail)
@ -667,7 +666,6 @@ TEST_CASE_FIXTURE(ClassFixture, "udtf_class_methods_works")
TEST_CASE_FIXTURE(ClassFixture, "write_of_readonly_is_nil")
{
ScopedFastFlag newSolver{FFlag::LuauSolverV2, true};
ScopedFastFlag udtfRwFix{FFlag::LuauUserTypeFunFixNoReadWrite, true};
CheckResult result = check(R"(
type function getclass(arg)

View file

@ -2,6 +2,7 @@
#include "Fixture.h"
#include "ScopedFlags.h"
#include "doctest.h"
#include "Luau/BuiltinDefinitions.h"
#include "Luau/AstQuery.h"
@ -9,6 +10,7 @@
using namespace Luau;
LUAU_FASTFLAG(LuauSolverV2)
LUAU_FASTFLAG(LuauFixInfiniteRecursionInNormalization)
TEST_SUITE_BEGIN("TypeAliases");
@ -1178,4 +1180,33 @@ TEST_CASE_FIXTURE(Fixture, "bound_type_in_alias_segfault")
)"));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "gh1632_no_infinite_recursion_in_normalization")
{
ScopedFastFlag flags[] = {
{FFlag::LuauSolverV2, true},
{FFlag::LuauFixInfiniteRecursionInNormalization, true},
};
CheckResult result = check(R"(
type Node<T> = {
value: T,
next: Node<T>?,
-- remove `prev`, solves issue
prev: Node<T>?,
};
type List<T> = {
head: Node<T>?
}
local function IsFront(list: List<any>, nodeB: Node<any>)
-- remove if statement below, solves issue
if (list.head == nodeB) then
end
end
)");
LUAU_CHECK_NO_ERRORS(result);
}
TEST_SUITE_END();

View file

@ -10,10 +10,9 @@
using namespace Luau;
LUAU_FASTFLAG(LuauSolverV2)
LUAU_FASTFLAG(LuauTypestateBuiltins2)
LUAU_FASTFLAG(LuauStringFormatArityFix)
LUAU_FASTFLAG(LuauTableCloneClonesType2)
LUAU_FASTFLAG(LuauTableCloneClonesType3)
LUAU_FASTFLAG(LuauStringFormatErrorSuppression)
LUAU_FASTFLAG(LuauFreezeIgnorePersistent)
TEST_SUITE_BEGIN("BuiltinTests");
@ -809,8 +808,6 @@ TEST_CASE_FIXTURE(Fixture, "string_format_as_method")
TEST_CASE_FIXTURE(BuiltinsFixture, "string_format_trivial_arity")
{
ScopedFastFlag sff{FFlag::LuauStringFormatArityFix, true};
CheckResult result = check(R"(
string.format()
)");
@ -1134,15 +1131,13 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "table_freeze_is_generic")
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
if (FFlag::LuauSolverV2 && FFlag::LuauTypestateBuiltins2)
if (FFlag::LuauSolverV2)
CHECK("Key 'b' not found in table '{ read a: number }'" == toString(result.errors[0]));
else if (FFlag::LuauSolverV2)
CHECK("Key 'b' not found in table '{ a: number }'" == toString(result.errors[0]));
else
CHECK_EQ("Key 'b' not found in table '{| a: number |}'", toString(result.errors[0]));
CHECK(Location({13, 18}, {13, 23}) == result.errors[0].location);
if (FFlag::LuauSolverV2 && FFlag::LuauTypestateBuiltins2)
if (FFlag::LuauSolverV2)
{
CHECK_EQ("{ read a: number }", toString(requireTypeAtPosition({15, 19})));
CHECK_EQ("{ read b: string }", toString(requireTypeAtPosition({16, 19})));
@ -1178,7 +1173,7 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "table_freeze_does_not_retroactively_block_mu
LUAU_REQUIRE_NO_ERRORS(result);
if (FFlag::LuauSolverV2 && FFlag::LuauTypestateBuiltins2)
if (FFlag::LuauSolverV2)
{
CHECK_EQ("{ a: number, q: string } | { read a: number, read q: string }", toString(requireType("t1"), {/*exhaustive */ true}));
// before the assignment, it's `t1`
@ -1208,7 +1203,6 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "table_freeze_no_generic_table")
end
)");
if (FFlag::LuauTypestateBuiltins2)
LUAU_REQUIRE_NO_ERRORS(result);
}
@ -1236,13 +1230,6 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "table_freeze_errors_on_no_args")
table.freeze()
)");
// this does not error in the new solver without the typestate builtins functionality.
if (FFlag::LuauSolverV2 && !FFlag::LuauTypestateBuiltins2)
{
LUAU_REQUIRE_NO_ERRORS(result);
return;
}
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK(get<CountMismatch>(result.errors[0]));
@ -1255,25 +1242,40 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "table_freeze_errors_on_non_tables")
table.freeze(42)
)");
// this does not error in the new solver without the typestate builtins functionality.
if (FFlag::LuauSolverV2 && !FFlag::LuauTypestateBuiltins2)
{
LUAU_REQUIRE_NO_ERRORS(result);
return;
}
LUAU_REQUIRE_ERROR_COUNT(1, result);
TypeMismatch* tm = get<TypeMismatch>(result.errors[0]);
REQUIRE(tm);
if (FFlag::LuauSolverV2 && FFlag::LuauTypestateBuiltins2)
if (FFlag::LuauSolverV2)
CHECK_EQ(toString(tm->wantedType), "table");
else
CHECK_EQ(toString(tm->wantedType), "{- -}");
CHECK_EQ(toString(tm->givenType), "number");
}
TEST_CASE_FIXTURE(BuiltinsFixture, "table_freeze_persistent_skip")
{
ScopedFastFlag luauFreezeIgnorePersistent{FFlag::LuauFreezeIgnorePersistent, true};
CheckResult result = check(R"(
table.freeze(table)
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(BuiltinsFixture, "table_clone_persistent_skip")
{
ScopedFastFlag luauFreezeIgnorePersistent{FFlag::LuauFreezeIgnorePersistent, true};
CheckResult result = check(R"(
table.clone(table)
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(BuiltinsFixture, "set_metatable_needs_arguments")
{
// In the new solver, nil can certainly be used where a generic is required, so all generic parameters are optional.
@ -1599,7 +1601,7 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "table_dot_clone_type_states")
LUAU_REQUIRE_NO_ERRORS(result);
if (FFlag::LuauTableCloneClonesType2)
if (FFlag::LuauTableCloneClonesType3)
{
CHECK_EQ(toString(requireType("t1"), {true}), "{ x: number, z: number }");
CHECK_EQ(toString(requireType("t2"), {true}), "{ x: number, y: number }");
@ -1611,6 +1613,40 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "table_dot_clone_type_states")
}
}
TEST_CASE_FIXTURE(BuiltinsFixture, "table_clone_should_not_break")
{
CheckResult result = check(R"(
local Immutable = {}
function Immutable.Set(dictionary, key, value)
local new = table.clone(dictionary)
new[key] = value
return new
end
return Immutable
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(BuiltinsFixture, "table_clone_should_not_break_2")
{
CheckResult result = check(R"(
function set(dictionary, key, value)
local new = table.clone(dictionary)
new[key] = value
return new
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(BuiltinsFixture, "string_format_should_support_any")
{
ScopedFastFlag _{FFlag::LuauSolverV2, true};

View file

@ -10,6 +10,7 @@
using namespace Luau;
LUAU_FASTFLAG(LuauNewSolverPrePopulateClasses)
LUAU_FASTFLAG(LuauClipNestedAndRecursiveUnion)
TEST_SUITE_BEGIN("DefinitionTests");
@ -541,4 +542,20 @@ TEST_CASE_FIXTURE(Fixture, "definition_file_has_source_module_name_set")
CHECK_EQ(ctv->definitionModuleName, "@test");
}
TEST_CASE_FIXTURE(Fixture, "recursive_redefinition_reduces_rightfully")
{
ScopedFastFlag _{FFlag::LuauClipNestedAndRecursiveUnion, true};
LUAU_REQUIRE_NO_ERRORS(check(R"(
local t: {[string]: string} = {}
local function f()
t = t
end
t = t
)"));
}
TEST_SUITE_END();

View file

@ -19,7 +19,6 @@ using namespace Luau;
LUAU_FASTFLAG(LuauInstantiateInSubtyping)
LUAU_FASTFLAG(LuauSolverV2)
LUAU_FASTINT(LuauTarjanChildLimit)
LUAU_FASTFLAG(LuauRetrySubtypingWithoutHiddenPack)
LUAU_FASTFLAG(DebugLuauEqSatSimplification)
TEST_SUITE_BEGIN("TypeInferFunctions");
@ -3012,9 +3011,6 @@ local u,v = id(3), id(id(44))
TEST_CASE_FIXTURE(Fixture, "hidden_variadics_should_not_break_subtyping")
{
// Only applies to new solver.
ScopedFastFlag sff{FFlag::LuauRetrySubtypingWithoutHiddenPack, true};
CheckResult result = check(R"(
--!strict
type FooType = {

View file

@ -12,7 +12,6 @@
LUAU_FASTFLAG(LuauInstantiateInSubtyping)
LUAU_FASTFLAG(LuauSolverV2)
LUAU_FASTFLAG(LuauTypestateBuiltins2)
LUAU_FASTFLAG(LuauNewSolverPopulateTableLocations)
using namespace Luau;
@ -185,10 +184,8 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "cross_module_table_freeze")
ModulePtr b = frontend.moduleResolver.getModule("game/B");
REQUIRE(b != nullptr);
// confirm that no cross-module mutation happened here!
if (FFlag::LuauSolverV2 && FFlag::LuauTypestateBuiltins2)
if (FFlag::LuauSolverV2)
CHECK(toString(b->returnType) == "{ read a: number }");
else if (FFlag::LuauSolverV2)
CHECK(toString(b->returnType) == "{ a: number }");
else
CHECK(toString(b->returnType) == "{| a: number |}");
}

View file

@ -17,6 +17,7 @@
using namespace Luau;
LUAU_FASTFLAG(LuauSolverV2)
LUAU_FASTFLAG(LuauDoNotGeneralizeInTypeFunctions)
TEST_SUITE_BEGIN("TypeInferOperators");
@ -814,19 +815,19 @@ TEST_CASE_FIXTURE(Fixture, "strict_binary_op_where_lhs_unknown")
TEST_CASE_FIXTURE(BuiltinsFixture, "and_binexps_dont_unify")
{
CheckResult result = check(R"(
ScopedFastFlag _{FFlag::LuauDoNotGeneralizeInTypeFunctions, true};
// `t` will be inferred to be of type `{ { test: unknown } }` which is
// reasonable, in that it's empty with no bounds on its members. Optimally
// we might emit an error here that the `print(...)` expression is
// unreachable.
LUAU_REQUIRE_NO_ERRORS(check(R"(
--!strict
local t = {}
while true and t[1] do
print(t[1].test)
end
)");
// This infers a type for `t` of `{unknown}`, and so it makes sense that `t[1].test` would error.
if (FFlag::LuauSolverV2)
LUAU_REQUIRE_ERROR_COUNT(1, result);
else
LUAU_REQUIRE_NO_ERRORS(result);
)"));
}
TEST_CASE_FIXTURE(Fixture, "error_on_invalid_operand_types_to_relational_operators")

View file

@ -1,4 +1,5 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#include "Luau/TypeInfer.h"
#include "Luau/RecursionCounter.h"
@ -12,6 +13,7 @@ using namespace Luau;
LUAU_FASTFLAG(LuauSolverV2);
LUAU_FASTFLAG(DebugLuauEqSatSimplification);
LUAU_FASTFLAG(LuauStoreCSTData);
LUAU_FASTINT(LuauNormalizeCacheLimit);
LUAU_FASTINT(LuauTarjanChildLimit);
LUAU_FASTINT(LuauTypeInferIterationLimit);
@ -46,7 +48,16 @@ TEST_CASE_FIXTURE(Fixture, "typeguard_inference_incomplete")
end
)";
const std::string expected = R"(
const std::string expected = FFlag::LuauStoreCSTData ? R"(
function f(a:{fn:()->(a,b...)}): ()
if type(a) == 'boolean' then
local a1:boolean=a
elseif a.fn() then
local a2:{fn:()->(a,b...)}=a
end
end
)"
: R"(
function f(a:{fn:()->(a,b...)}): ()
if type(a) == 'boolean'then
local a1:boolean=a
@ -56,7 +67,16 @@ TEST_CASE_FIXTURE(Fixture, "typeguard_inference_incomplete")
end
)";
const std::string expectedWithNewSolver = R"(
const std::string expectedWithNewSolver = FFlag::LuauStoreCSTData ? R"(
function f(a:{fn:()->(unknown,...unknown)}): ()
if type(a) == 'boolean' then
local a1:{fn:()->(unknown,...unknown)}&boolean=a
elseif a.fn() then
local a2:{fn:()->(unknown,...unknown)}&(class|function|nil|number|string|thread|buffer|table)=a
end
end
)"
: R"(
function f(a:{fn:()->(unknown,...unknown)}): ()
if type(a) == 'boolean'then
local a1:{fn:()->(unknown,...unknown)}&boolean=a
@ -66,7 +86,16 @@ TEST_CASE_FIXTURE(Fixture, "typeguard_inference_incomplete")
end
)";
const std::string expectedWithEqSat = R"(
const std::string expectedWithEqSat = FFlag::LuauStoreCSTData ? R"(
function f(a:{fn:()->(unknown,...unknown)}): ()
if type(a) == 'boolean' then
local a1:{fn:()->(unknown,...unknown)}&boolean=a
elseif a.fn() then
local a2:{fn:()->(unknown,...unknown)}&negate<boolean>=a
end
end
)"
: R"(
function f(a:{fn:()->(unknown,...unknown)}): ()
if type(a) == 'boolean'then
local a1:{fn:()->(unknown,...unknown)}&boolean=a
@ -535,10 +564,10 @@ TEST_CASE_FIXTURE(Fixture, "free_options_cannot_be_unified_together")
std::unique_ptr scope = std::make_unique<Scope>(builtinTypes->anyTypePack);
TypeId free1 = arena.addType(FreeType{scope.get()});
TypeId free1 = arena.freshType(builtinTypes, scope.get());
TypeId option1 = arena.addType(UnionType{{nilType, free1}});
TypeId free2 = arena.addType(FreeType{scope.get()});
TypeId free2 = arena.freshType(builtinTypes, scope.get());
TypeId option2 = arena.addType(UnionType{{nilType, free2}});
InternalErrorReporter iceHandler;
@ -965,10 +994,10 @@ TEST_CASE_FIXTURE(Fixture, "free_options_can_be_unified_together")
std::unique_ptr scope = std::make_unique<Scope>(builtinTypes->anyTypePack);
TypeId free1 = arena.addType(FreeType{scope.get()});
TypeId free1 = arena.freshType(builtinTypes, scope.get());
TypeId option1 = arena.addType(UnionType{{nilType, free1}});
TypeId free2 = arena.addType(FreeType{scope.get()});
TypeId free2 = arena.freshType(builtinTypes, scope.get());
TypeId option2 = arena.addType(UnionType{{nilType, free2}});
InternalErrorReporter iceHandler;
@ -1284,7 +1313,7 @@ TEST_CASE_FIXTURE(Fixture, "table_containing_non_final_type_is_erroneously_cache
TableType* table = getMutable<TableType>(tableTy);
REQUIRE(table);
TypeId freeTy = arena.freshType(&globalScope);
TypeId freeTy = arena.freshType(builtinTypes, &globalScope);
table->props["foo"] = Property::rw(freeTy);

View file

@ -18,7 +18,6 @@ using namespace Luau;
LUAU_FASTFLAG(LuauSolverV2)
LUAU_FASTFLAG(LuauInstantiateInSubtyping)
LUAU_FASTFLAG(LuauFixIndexerSubtypingOrdering)
LUAU_FASTFLAG(LuauRetrySubtypingWithoutHiddenPack)
LUAU_FASTFLAG(LuauTableKeysAreRValues)
LUAU_FASTFLAG(LuauAllowNilAssignmentToIndexer)
LUAU_FASTFLAG(LuauTrackInteriorFreeTypesOnScope)
@ -2378,7 +2377,7 @@ TEST_CASE_FIXTURE(Fixture, "invariant_table_properties_means_instantiating_table
local c : string = t.m("hi")
)");
if (FFlag::LuauSolverV2 && FFlag::LuauRetrySubtypingWithoutHiddenPack)
if (FFlag::LuauSolverV2)
{
LUAU_REQUIRE_ERROR_COUNT(1, result);
@ -2387,15 +2386,6 @@ TEST_CASE_FIXTURE(Fixture, "invariant_table_properties_means_instantiating_table
// This is not actually the expected behavior, but the typemismatch we were seeing before was for the wrong reason.
// The behavior of this test is just regressed generally in the new solver, and will need to be consciously addressed.
}
else if (FFlag::LuauSolverV2)
{
LUAU_REQUIRE_ERROR_COUNT(2, result);
CHECK(get<TypeMismatch>(result.errors[0]));
CHECK(Location{{6, 45}, {6, 46}} == result.errors[0].location);
CHECK(get<ExplicitFunctionAnnotationRecommended>(result.errors[1]));
}
// TODO: test behavior is wrong with LuauInstantiateInSubtyping until we can re-enable the covariant requirement for instantiation in subtyping
else if (FFlag::LuauInstantiateInSubtyping)

View file

@ -24,6 +24,7 @@ LUAU_FASTINT(LuauNormalizeCacheLimit);
LUAU_FASTINT(LuauRecursionLimit);
LUAU_FASTINT(LuauTypeInferRecursionLimit);
LUAU_FASTFLAG(InferGlobalTypes)
LUAU_FASTFLAG(LuauAstTypeGroup)
using namespace Luau;
@ -1201,6 +1202,9 @@ TEST_CASE_FIXTURE(Fixture, "type_infer_recursion_limit_normalizer")
CHECK(3 == result.errors.size());
CHECK(Location{{2, 22}, {2, 41}} == result.errors[0].location);
CHECK(Location{{3, 22}, {3, 42}} == result.errors[1].location);
if (FFlag::LuauAstTypeGroup)
CHECK(Location{{3, 22}, {3, 40}} == result.errors[2].location);
else
CHECK(Location{{3, 23}, {3, 40}} == result.errors[2].location);
CHECK_EQ("Code is too complex to typecheck! Consider simplifying the code around this area", toString(result.errors[0]));
CHECK_EQ("Code is too complex to typecheck! Consider simplifying the code around this area", toString(result.errors[1]));

View file

@ -42,12 +42,13 @@ TEST_CASE_FIXTURE(TryUnifyFixture, "primitives_unify")
TEST_CASE_FIXTURE(TryUnifyFixture, "compatible_functions_are_unified")
{
Type functionOne{TypeVariant{FunctionType(arena.addTypePack({arena.freshType(globalScope->level)}), arena.addTypePack({builtinTypes->numberType}))
Type functionOne{TypeVariant{
FunctionType(arena.addTypePack({arena.freshType(builtinTypes, globalScope->level)}), arena.addTypePack({builtinTypes->numberType}))
}};
Type functionTwo{
TypeVariant{FunctionType(arena.addTypePack({arena.freshType(globalScope->level)}), arena.addTypePack({arena.freshType(globalScope->level)}))}
};
Type functionTwo{TypeVariant{FunctionType(
arena.addTypePack({arena.freshType(builtinTypes, globalScope->level)}), arena.addTypePack({arena.freshType(builtinTypes, globalScope->level)})
)}};
state.tryUnify(&functionTwo, &functionOne);
CHECK(!state.failure);
@ -60,14 +61,16 @@ TEST_CASE_FIXTURE(TryUnifyFixture, "compatible_functions_are_unified")
TEST_CASE_FIXTURE(TryUnifyFixture, "incompatible_functions_are_preserved")
{
TypePackVar argPackOne{TypePack{{arena.freshType(globalScope->level)}, std::nullopt}};
Type functionOne{TypeVariant{FunctionType(arena.addTypePack({arena.freshType(globalScope->level)}), arena.addTypePack({builtinTypes->numberType}))
TypePackVar argPackOne{TypePack{{arena.freshType(builtinTypes, globalScope->level)}, std::nullopt}};
Type functionOne{TypeVariant{
FunctionType(arena.addTypePack({arena.freshType(builtinTypes, globalScope->level)}), arena.addTypePack({builtinTypes->numberType}))
}};
Type functionOneSaved = functionOne.clone();
TypePackVar argPackTwo{TypePack{{arena.freshType(globalScope->level)}, std::nullopt}};
Type functionTwo{TypeVariant{FunctionType(arena.addTypePack({arena.freshType(globalScope->level)}), arena.addTypePack({builtinTypes->stringType}))
TypePackVar argPackTwo{TypePack{{arena.freshType(builtinTypes, globalScope->level)}, std::nullopt}};
Type functionTwo{TypeVariant{
FunctionType(arena.addTypePack({arena.freshType(builtinTypes, globalScope->level)}), arena.addTypePack({builtinTypes->stringType}))
}};
Type functionTwoSaved = functionTwo.clone();
@ -83,11 +86,11 @@ TEST_CASE_FIXTURE(TryUnifyFixture, "incompatible_functions_are_preserved")
TEST_CASE_FIXTURE(TryUnifyFixture, "tables_can_be_unified")
{
Type tableOne{TypeVariant{
TableType{{{"foo", {arena.freshType(globalScope->level)}}}, std::nullopt, globalScope->level, TableState::Unsealed},
TableType{{{"foo", {arena.freshType(builtinTypes, globalScope->level)}}}, std::nullopt, globalScope->level, TableState::Unsealed},
}};
Type tableTwo{TypeVariant{
TableType{{{"foo", {arena.freshType(globalScope->level)}}}, std::nullopt, globalScope->level, TableState::Unsealed},
TableType{{{"foo", {arena.freshType(builtinTypes, globalScope->level)}}}, std::nullopt, globalScope->level, TableState::Unsealed},
}};
CHECK_NE(*getMutable<TableType>(&tableOne)->props["foo"].type(), *getMutable<TableType>(&tableTwo)->props["foo"].type());
@ -106,7 +109,7 @@ TEST_CASE_FIXTURE(TryUnifyFixture, "incompatible_tables_are_preserved")
{
Type tableOne{TypeVariant{
TableType{
{{"foo", {arena.freshType(globalScope->level)}}, {"bar", {builtinTypes->numberType}}},
{{"foo", {arena.freshType(builtinTypes, globalScope->level)}}, {"bar", {builtinTypes->numberType}}},
std::nullopt,
globalScope->level,
TableState::Unsealed
@ -115,7 +118,7 @@ TEST_CASE_FIXTURE(TryUnifyFixture, "incompatible_tables_are_preserved")
Type tableTwo{TypeVariant{
TableType{
{{"foo", {arena.freshType(globalScope->level)}}, {"bar", {builtinTypes->stringType}}},
{{"foo", {arena.freshType(builtinTypes, globalScope->level)}}, {"bar", {builtinTypes->stringType}}},
std::nullopt,
globalScope->level,
TableState::Unsealed
@ -295,7 +298,7 @@ TEST_CASE_FIXTURE(TryUnifyFixture, "free_tail_is_grown_properly")
TEST_CASE_FIXTURE(TryUnifyFixture, "recursive_metatable_getmatchtag")
{
Type redirect{FreeType{TypeLevel{}}};
Type redirect{FreeType{TypeLevel{}, builtinTypes->neverType, builtinTypes->unknownType}};
Type table{TableType{}};
Type metatable{MetatableType{&redirect, &table}};
redirect = BoundType{&metatable}; // Now we have a metatable that is recursive on the table type
@ -318,7 +321,7 @@ TEST_CASE_FIXTURE(TryUnifyFixture, "cli_50320_follow_in_any_unification")
TEST_CASE_FIXTURE(TryUnifyFixture, "txnlog_preserves_type_owner")
{
TypeId a = arena.addType(Type{FreeType{TypeLevel{}}});
TypeId a = arena.freshType(builtinTypes, TypeLevel{});
TypeId b = builtinTypes->numberType;
state.tryUnify(a, b);
@ -381,7 +384,7 @@ TEST_CASE_FIXTURE(TryUnifyFixture, "fuzz_tail_unification_issue")
TypePackVar packTmp{TypePack{{builtinTypes->anyType}, &variadicAny}};
TypePackVar packSub{TypePack{{builtinTypes->anyType, builtinTypes->anyType}, &packTmp}};
Type freeTy{FreeType{TypeLevel{}}};
Type freeTy{FreeType{TypeLevel{}, builtinTypes->neverType, builtinTypes->unknownType}};
TypePackVar freeTp{FreeTypePack{TypeLevel{}}};
TypePackVar packSuper{TypePack{{&freeTy}, &freeTp}};
@ -438,10 +441,10 @@ TEST_CASE_FIXTURE(TryUnifyFixture, "unifying_two_unions_under_dcr_does_not_creat
const std::shared_ptr<Scope> scope = globalScope;
const std::shared_ptr<Scope> nestedScope = std::make_shared<Scope>(scope);
const TypeId outerType = arena.freshType(scope.get());
const TypeId outerType2 = arena.freshType(scope.get());
const TypeId outerType = arena.freshType(builtinTypes, scope.get());
const TypeId outerType2 = arena.freshType(builtinTypes, scope.get());
const TypeId innerType = arena.freshType(nestedScope.get());
const TypeId innerType = arena.freshType(builtinTypes, nestedScope.get());
state.enableNewSolver();

View file

@ -621,7 +621,7 @@ TEST_CASE_FIXTURE(Fixture, "indexing_into_a_cyclic_union_doesnt_crash")
TypeArena& arena = frontend.globals.globalTypes;
unfreeze(arena);
TypeId badCyclicUnionTy = arena.freshType(frontend.globals.globalScope.get());
TypeId badCyclicUnionTy = arena.freshType(builtinTypes, frontend.globals.globalScope.get());
UnionType u;
u.options.push_back(badCyclicUnionTy);

View file

@ -17,6 +17,7 @@ using namespace Luau::TypePath;
LUAU_FASTFLAG(LuauSolverV2);
LUAU_DYNAMIC_FASTINT(LuauTypePathMaximumTraverseSteps);
LUAU_FASTFLAG(LuauFreeTypesMustHaveBounds);
struct TypePathFixture : Fixture
{
@ -277,7 +278,7 @@ TEST_CASE_FIXTURE(TypePathFixture, "bounds")
TypeArena& arena = frontend.globals.globalTypes;
unfreeze(arena);
TypeId ty = arena.freshType(frontend.globals.globalScope.get());
TypeId ty = arena.freshType(frontend.builtinTypes, frontend.globals.globalScope.get());
FreeType* ft = getMutable<FreeType>(ty);
SUBCASE("upper")

View file

@ -219,7 +219,7 @@ TEST_CASE_FIXTURE(Fixture, "UnionTypeIterator_with_only_cyclic_union")
*/
TEST_CASE_FIXTURE(Fixture, "substitution_skip_failure")
{
Type ftv11{FreeType{TypeLevel{}}};
Type ftv11{FreeType{TypeLevel{}, builtinTypes->neverType, builtinTypes->unknownType}};
TypePackVar tp24{TypePack{{&ftv11}}};
TypePackVar tp17{TypePack{}};
@ -469,8 +469,8 @@ TEST_CASE("content_reassignment")
myAny.documentationSymbol = "@global/any";
TypeArena arena;
TypeId futureAny = arena.addType(FreeType{TypeLevel{}});
BuiltinTypes builtinTypes;
TypeId futureAny = arena.freshType(NotNull{&builtinTypes}, TypeLevel{});
asMutable(futureAny)->reassign(myAny);
CHECK(get<AnyType>(futureAny) != nullptr);

View file

@ -4,6 +4,7 @@
#include "Luau/RecursionCounter.h"
#include "Luau/Type.h"
#include "doctest.h"
using namespace Luau;
@ -54,7 +55,7 @@ TEST_CASE_FIXTURE(Fixture, "dont_throw_when_limit_is_high_enough")
TEST_CASE_FIXTURE(Fixture, "some_free_types_do_not_have_bounds")
{
Type t{FreeType{TypeLevel{}}};
Type t{FreeType{TypeLevel{}, builtinTypes->neverType, builtinTypes->unknownType}};
(void)toString(&t);
}