luau/Analysis/src/TypeFunction.cpp
Vighnesh-V 8863bfc950
Sync to upstream/release/686 (#1948)
## General
This week has been spent mostly on fixing bugs in incremental
autocomplete as well as making the new Type Solver more stable.

- Fixes a bug where registered "require" aliases were case-sensitive
instead of case-insensitive.
### New Type Solver
- Adjust literal sub typing logic to account for unreduced type
functions
- Implement a number of subtyping stack utilization improvements
- Emit a single error if an internal type escapes a module's interface
- Checked function errors in the New Non Strict warn about incorrect
argument use with one-indexed positions, e.g. `argument #1 was used
incorrectly` instead of `argument #0 was used incorrectly`.
- Improvements to type function reduction that let us progress further
while reducing
- Augment the generalization system to not emit duplicate constraints.
- Fix a bug where we didn't seal tables in modules that failed to
complete typechecking.

### Fragment Autocomplete
- Provide richer autocomplete suggestions inside of for loops
- Provide richer autocomplete suggestions inside of interpolated string
expressions
- Improve the quality of error messages when typing out interpolated
strings.

### Compiler
- Fixes REX encoding of extended byte registers for the x86 assembly
code generation.
- Fixes for table shape constant data encoding

---
Co-authored-by: Andy Friesen <afriesen@roblox.com>
Co-authored-by: Ariel Weiss <aaronweiss@roblox.com>
Co-authored-by: Hunter Goldstein <hgoldstein@roblox.com>
Co-authored-by: Sora Kanosue <skanosue@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>
2025-08-08 10:18:16 -07:00

788 lines
25 KiB
C++

// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#include "Luau/TypeFunction.h"
#include "Luau/Common.h"
#include "Luau/ConstraintSolver.h"
#include "Luau/DenseHash.h"
#include "Luau/Normalize.h"
#include "Luau/NotNull.h"
#include "Luau/OverloadResolution.h"
#include "Luau/Subtyping.h"
#include "Luau/ToString.h"
#include "Luau/TxnLog.h"
#include "Luau/Type.h"
#include "Luau/TypeChecker2.h"
#include "Luau/TypeFunctionReductionGuesser.h"
#include "Luau/TypeFwd.h"
#include "Luau/TypeUtils.h"
#include "Luau/Unifier2.h"
#include "Luau/VecDeque.h"
#include "Luau/VisitType.h"
// used to control emitting CodeTooComplex warnings on type function reduction
LUAU_DYNAMIC_FASTINTVARIABLE(LuauTypeFamilyGraphReductionMaximumSteps, 1'000'000);
// used to control the limits of type function application over union type arguments
// e.g. `mul<a | b, c | d>` blows up into `mul<a, c> | mul<a, d> | mul<b, c> | mul<b, d>`
LUAU_DYNAMIC_FASTINTVARIABLE(LuauTypeFamilyApplicationCartesianProductLimit, 5'000);
// used to control falling back to a more conservative reduction based on guessing
// when this value is set to a negative value, guessing will be totally disabled.
LUAU_DYNAMIC_FASTINTVARIABLE(LuauTypeFamilyUseGuesserDepth, -1);
LUAU_FASTFLAG(DebugLuauEqSatSimplification)
LUAU_FASTFLAG(LuauEagerGeneralization4)
LUAU_FASTFLAGVARIABLE(DebugLuauLogTypeFamilies)
LUAU_FASTFLAG(LuauUpdateGetMetatableTypeSignature)
LUAU_FASTFLAGVARIABLE(LuauOccursCheckForRefinement)
LUAU_FASTFLAG(LuauRefineTablesWithReadType)
LUAU_FASTFLAGVARIABLE(LuauEmptyStringInKeyOf)
LUAU_FASTFLAGVARIABLE(LuauAvoidExcessiveTypeCopying)
namespace Luau
{
using TypeOrTypePackIdSet = DenseHashSet<const void*>;
struct InstanceCollector : TypeOnceVisitor
{
DenseHashSet<TypeId> recordedTys{nullptr};
VecDeque<TypeId> tys;
DenseHashSet<TypePackId> recordedTps{nullptr};
VecDeque<TypePackId> tps;
TypeOrTypePackIdSet shouldGuess{nullptr};
std::vector<const void*> typeFunctionInstanceStack;
std::vector<TypeId> cyclicInstance;
InstanceCollector()
: TypeOnceVisitor("InstanceCollector")
{
}
bool visit(TypeId ty, const TypeFunctionInstanceType& tfit) override
{
// TypeVisitor performs a depth-first traversal in the absence of
// cycles. This means that by pushing to the front of the queue, we will
// try to reduce deeper instances first if we start with the first thing
// in the queue. Consider Add<Add<Add<number, number>, number>, number>:
// we want to reduce the innermost Add<number, number> instantiation
// first.
typeFunctionInstanceStack.push_back(ty);
if (DFInt::LuauTypeFamilyUseGuesserDepth >= 0 && int(typeFunctionInstanceStack.size()) > DFInt::LuauTypeFamilyUseGuesserDepth)
shouldGuess.insert(ty);
if (!recordedTys.contains(ty))
{
recordedTys.insert(ty);
tys.push_front(ty);
}
for (TypeId p : tfit.typeArguments)
traverse(p);
for (TypePackId p : tfit.packArguments)
traverse(p);
typeFunctionInstanceStack.pop_back();
return false;
}
void cycle(TypeId ty) override
{
TypeId t = follow(ty);
if (get<TypeFunctionInstanceType>(t))
{
// If we see a type a second time and it's in the type function stack, it's a real cycle
if (std::find(typeFunctionInstanceStack.begin(), typeFunctionInstanceStack.end(), t) != typeFunctionInstanceStack.end())
cyclicInstance.push_back(t);
}
}
bool visit(TypeId ty, const ExternType&) override
{
return false;
}
bool visit(TypePackId tp, const TypeFunctionInstanceTypePack& tfitp) override
{
// TypeVisitor performs a depth-first traversal in the absence of
// cycles. This means that by pushing to the front of the queue, we will
// try to reduce deeper instances first if we start with the first thing
// in the queue. Consider Add<Add<Add<number, number>, number>, number>:
// we want to reduce the innermost Add<number, number> instantiation
// first.
typeFunctionInstanceStack.push_back(tp);
if (DFInt::LuauTypeFamilyUseGuesserDepth >= 0 && int(typeFunctionInstanceStack.size()) > DFInt::LuauTypeFamilyUseGuesserDepth)
shouldGuess.insert(tp);
if (!recordedTps.contains(tp))
{
recordedTps.insert(tp);
tps.push_front(tp);
}
for (TypeId p : tfitp.typeArguments)
traverse(p);
for (TypePackId p : tfitp.packArguments)
traverse(p);
typeFunctionInstanceStack.pop_back();
return false;
}
};
struct UnscopedGenericFinder : TypeOnceVisitor
{
std::vector<TypeId> scopeGenTys;
std::vector<TypePackId> scopeGenTps;
bool foundUnscoped = false;
UnscopedGenericFinder()
: TypeOnceVisitor("UnscopedGenericFinder")
{
}
bool visit(TypeId ty) override
{
// Once we have found an unscoped generic, we will stop the traversal
return !foundUnscoped;
}
bool visit(TypePackId tp) override
{
// Once we have found an unscoped generic, we will stop the traversal
return !foundUnscoped;
}
bool visit(TypeId ty, const GenericType&) override
{
if (std::find(scopeGenTys.begin(), scopeGenTys.end(), ty) == scopeGenTys.end())
foundUnscoped = true;
return false;
}
bool visit(TypePackId tp, const GenericTypePack&) override
{
if (std::find(scopeGenTps.begin(), scopeGenTps.end(), tp) == scopeGenTps.end())
foundUnscoped = true;
return false;
}
bool visit(TypeId ty, const FunctionType& ftv) override
{
size_t startTyCount = scopeGenTys.size();
size_t startTpCount = scopeGenTps.size();
scopeGenTys.insert(scopeGenTys.end(), ftv.generics.begin(), ftv.generics.end());
scopeGenTps.insert(scopeGenTps.end(), ftv.genericPacks.begin(), ftv.genericPacks.end());
traverse(ftv.argTypes);
traverse(ftv.retTypes);
scopeGenTys.resize(startTyCount);
scopeGenTps.resize(startTpCount);
return false;
}
bool visit(TypeId ty, const ExternType&) override
{
return false;
}
};
struct TypeFunctionReducer
{
NotNull<TypeFunctionContext> ctx;
VecDeque<TypeId> queuedTys;
VecDeque<TypePackId> queuedTps;
TypeOrTypePackIdSet shouldGuess;
std::vector<TypeId> cyclicTypeFunctions;
TypeOrTypePackIdSet irreducible{nullptr};
FunctionGraphReductionResult result;
bool force = false;
// Local to the constraint being reduced.
Location location;
TypeFunctionReducer(
VecDeque<TypeId> queuedTys,
VecDeque<TypePackId> queuedTps,
TypeOrTypePackIdSet shouldGuess,
std::vector<TypeId> cyclicTypes,
Location location,
NotNull<TypeFunctionContext> ctx,
bool force = false
)
: ctx(ctx)
, queuedTys(std::move(queuedTys))
, queuedTps(std::move(queuedTps))
, shouldGuess(std::move(shouldGuess))
, cyclicTypeFunctions(std::move(cyclicTypes))
, force(force)
, location(location)
{
}
enum class SkipTestResult
{
/// If a type function is cyclic, it cannot be reduced, but maybe we can
/// make a guess and offer a suggested annotation to the user.
CyclicTypeFunction,
/// Indicase that we will not be able to reduce this type function this
/// time. Constraint resolution may cause this type function to become
/// reducible later.
Irreducible,
/// A type function that cannot be reduced any further because it has no valid reduction.
/// eg add<number, string>
Stuck,
/// Some type functions can operate on generic parameters
Generic,
/// We might be able to reduce this type function, but not yet.
Defer,
/// We can attempt to reduce this type function right now.
Okay,
};
SkipTestResult DEPRECATED_testForSkippability(TypeId ty)
{
ty = follow(ty);
if (is<TypeFunctionInstanceType>(ty))
{
for (auto t : cyclicTypeFunctions)
{
if (ty == t)
return SkipTestResult::CyclicTypeFunction;
}
if (!irreducible.contains(ty))
return SkipTestResult::Defer;
return SkipTestResult::Irreducible;
}
else if (is<GenericType>(ty))
{
if (FFlag::LuauEagerGeneralization4)
return SkipTestResult::Generic;
else
return SkipTestResult::Irreducible;
}
return SkipTestResult::Okay;
}
SkipTestResult testForSkippability(TypeId ty)
{
if (!FFlag::LuauEagerGeneralization4)
return DEPRECATED_testForSkippability(ty);
VecDeque<TypeId> queue;
DenseHashSet<TypeId> seen{nullptr};
queue.push_back(follow(ty));
while (!queue.empty())
{
TypeId t = queue.front();
queue.pop_front();
if (seen.contains(t))
continue;
if (auto tfit = get<TypeFunctionInstanceType>(t))
{
if (FFlag::LuauEagerGeneralization4)
{
if (tfit->state == TypeFunctionInstanceState::Stuck)
return SkipTestResult::Stuck;
else if (tfit->state == TypeFunctionInstanceState::Solved)
return SkipTestResult::Generic;
}
for (auto cyclicTy : cyclicTypeFunctions)
{
if (t == cyclicTy)
return SkipTestResult::CyclicTypeFunction;
}
if (!irreducible.contains(t))
return SkipTestResult::Defer;
return SkipTestResult::Irreducible;
}
else if (is<GenericType>(t))
return SkipTestResult::Generic;
else if (auto it = get<IntersectionType>(t))
{
for (TypeId part : it->parts)
queue.push_back(follow(part));
}
seen.insert(t);
}
return SkipTestResult::Okay;
}
SkipTestResult testForSkippability(TypePackId ty) const
{
ty = follow(ty);
if (is<TypeFunctionInstanceTypePack>(ty))
{
if (!irreducible.contains(ty))
return SkipTestResult::Defer;
else
return SkipTestResult::Irreducible;
}
else if (is<GenericTypePack>(ty))
{
if (FFlag::LuauEagerGeneralization4)
return SkipTestResult::Generic;
else
return SkipTestResult::Irreducible;
}
return SkipTestResult::Okay;
}
template<typename T>
void replace(T subject, T replacement)
{
if (subject->owningArena != ctx->arena.get())
{
result.errors.emplace_back(location, InternalError{"Attempting to modify a type function instance from another arena"});
return;
}
if (FFlag::DebugLuauLogTypeFamilies)
printf("%s => %s\n", toString(subject, {true}).c_str(), toString(replacement, {true}).c_str());
asMutable(subject)->ty.template emplace<Unifiable::Bound<T>>(replacement);
if constexpr (std::is_same_v<T, TypeId>)
result.reducedTypes.insert(subject);
else if constexpr (std::is_same_v<T, TypePackId>)
result.reducedPacks.insert(subject);
}
TypeFunctionInstanceState getState(TypeId ty) const
{
auto tfit = get<TypeFunctionInstanceType>(ty);
LUAU_ASSERT(tfit);
return tfit->state;
}
void setState(TypeId ty, TypeFunctionInstanceState state) const
{
if (ty->owningArena != ctx->arena)
return;
TypeFunctionInstanceType* tfit = getMutable<TypeFunctionInstanceType>(ty);
LUAU_ASSERT(tfit);
tfit->state = state;
}
TypeFunctionInstanceState getState(TypePackId tp) const
{
return TypeFunctionInstanceState::Unsolved;
}
void setState(TypePackId tp, TypeFunctionInstanceState state) const
{
// We do not presently have any type pack functions at all.
(void)tp;
(void)state;
}
template<typename T>
void handleTypeFunctionReduction(T subject, TypeFunctionReductionResult<T> reduction)
{
for (auto& message : reduction.messages)
result.messages.emplace_back(location, UserDefinedTypeFunctionError{std::move(message)});
if (reduction.result)
replace(subject, *reduction.result);
else
{
irreducible.insert(subject);
if (reduction.error.has_value())
result.errors.emplace_back(location, UserDefinedTypeFunctionError{*reduction.error});
if (reduction.reductionStatus != Reduction::MaybeOk || force)
{
if (FFlag::DebugLuauLogTypeFamilies)
printf("%s is uninhabited\n", toString(subject, {true}).c_str());
if (FFlag::LuauEagerGeneralization4)
{
if (getState(subject) == TypeFunctionInstanceState::Unsolved)
{
if (reduction.reductionStatus == Reduction::Erroneous)
setState(subject, TypeFunctionInstanceState::Stuck);
else if (reduction.reductionStatus == Reduction::Irreducible)
setState(subject, TypeFunctionInstanceState::Solved);
else if (reduction.reductionStatus == Reduction::MaybeOk)
{
// We cannot make progress because something is unsolved, but we're also forcing.
setState(subject, TypeFunctionInstanceState::Stuck);
}
else
ctx->ice->ice("Unexpected TypeFunctionInstanceState");
}
}
if constexpr (std::is_same_v<T, TypeId>)
result.errors.emplace_back(location, UninhabitedTypeFunction{subject});
else if constexpr (std::is_same_v<T, TypePackId>)
result.errors.emplace_back(location, UninhabitedTypePackFunction{subject});
}
else if (reduction.reductionStatus == Reduction::MaybeOk && !force)
{
// We're not forcing and the reduction couldn't proceed, but it isn't obviously busted.
// Report that this type blocks further reduction.
if (FFlag::DebugLuauLogTypeFamilies)
printf(
"%s is irreducible; blocked on %zu types, %zu packs\n",
toString(subject, {true}).c_str(),
reduction.blockedTypes.size(),
reduction.blockedPacks.size()
);
for (TypeId b : reduction.blockedTypes)
result.blockedTypes.insert(b);
for (TypePackId b : reduction.blockedPacks)
result.blockedPacks.insert(b);
}
else
LUAU_ASSERT(!"Unreachable");
}
}
bool done() const
{
return queuedTys.empty() && queuedTps.empty();
}
template<typename T, typename I>
bool testParameters(T subject, const I* tfit)
{
for (TypeId p : tfit->typeArguments)
{
SkipTestResult skip = testForSkippability(p);
if (skip == SkipTestResult::Stuck)
{
// SkipTestResult::Stuck cannot happen when this flag is unset.
LUAU_ASSERT(FFlag::LuauEagerGeneralization4);
if (FFlag::DebugLuauLogTypeFamilies)
printf("%s is stuck!\n", toString(subject, {true}).c_str());
irreducible.insert(subject);
setState(subject, TypeFunctionInstanceState::Stuck);
return false;
}
if (skip == SkipTestResult::Irreducible || (skip == SkipTestResult::Generic && !tfit->function->canReduceGenerics))
{
if (FFlag::DebugLuauLogTypeFamilies)
{
if (skip == SkipTestResult::Generic)
printf("%s is solved due to a dependency on %s\n", toString(subject, {true}).c_str(), toString(p, {true}).c_str());
else
printf("%s is irreducible due to a dependency on %s\n", toString(subject, {true}).c_str(), toString(p, {true}).c_str());
}
irreducible.insert(subject);
if (skip == SkipTestResult::Generic)
setState(subject, TypeFunctionInstanceState::Solved);
return false;
}
else if (skip == SkipTestResult::Defer)
{
if (FFlag::DebugLuauLogTypeFamilies)
printf("Deferring %s until %s is solved\n", toString(subject, {true}).c_str(), toString(p, {true}).c_str());
if constexpr (std::is_same_v<T, TypeId>)
queuedTys.push_back(subject);
else if constexpr (std::is_same_v<T, TypePackId>)
queuedTps.push_back(subject);
return false;
}
}
for (TypePackId p : tfit->packArguments)
{
SkipTestResult skip = testForSkippability(p);
if (skip == SkipTestResult::Irreducible || (skip == SkipTestResult::Generic && !tfit->function->canReduceGenerics))
{
if (FFlag::DebugLuauLogTypeFamilies)
printf("%s is irreducible due to a dependency on %s\n", toString(subject, {true}).c_str(), toString(p, {true}).c_str());
irreducible.insert(subject);
return false;
}
else if (skip == SkipTestResult::Defer)
{
if (FFlag::DebugLuauLogTypeFamilies)
printf("Deferring %s until %s is solved\n", toString(subject, {true}).c_str(), toString(p, {true}).c_str());
if constexpr (std::is_same_v<T, TypeId>)
queuedTys.push_back(subject);
else if constexpr (std::is_same_v<T, TypePackId>)
queuedTps.push_back(subject);
return false;
}
}
return true;
}
template<typename TID>
inline bool tryGuessing(TID subject)
{
if (shouldGuess.contains(subject))
{
if (FFlag::DebugLuauLogTypeFamilies)
printf("Flagged %s for reduction with guesser.\n", toString(subject, {true}).c_str());
TypeFunctionReductionGuesser guesser{ctx->arena, ctx->builtins, ctx->normalizer};
auto guessed = guesser.guess(subject);
if (guessed)
{
if (FFlag::DebugLuauLogTypeFamilies)
printf("Selected %s as the guessed result type.\n", toString(*guessed, {true}).c_str());
replace(subject, *guessed);
return true;
}
if (FFlag::DebugLuauLogTypeFamilies)
printf("Failed to produce a guess for the result of %s.\n", toString(subject, {true}).c_str());
}
return false;
}
void stepType()
{
TypeId subject = follow(queuedTys.front());
queuedTys.pop_front();
if (irreducible.contains(subject))
return;
if (FFlag::DebugLuauLogTypeFamilies)
printf("Trying to %sreduce %s\n", force ? "force " : "", toString(subject, {true}).c_str());
if (const TypeFunctionInstanceType* tfit = get<TypeFunctionInstanceType>(subject))
{
if (tfit->function->name == "user")
{
UnscopedGenericFinder finder;
finder.traverse(subject);
if (finder.foundUnscoped)
{
// Do not step into this type again
irreducible.insert(subject);
// Let the caller know this type will not become reducible
result.irreducibleTypes.insert(subject);
if (FFlag::DebugLuauLogTypeFamilies)
printf("Irreducible due to an unscoped generic type\n");
return;
}
}
SkipTestResult testCyclic = testForSkippability(subject);
if (!testParameters(subject, tfit) && testCyclic != SkipTestResult::CyclicTypeFunction)
{
if (FFlag::DebugLuauLogTypeFamilies)
printf("Irreducible due to irreducible/pending and a non-cyclic function\n");
if (tfit->state == TypeFunctionInstanceState::Stuck || tfit->state == TypeFunctionInstanceState::Solved)
tryGuessing(subject);
return;
}
if (tryGuessing(subject))
return;
ctx->userFuncName = tfit->userFuncName;
TypeFunctionReductionResult<TypeId> result = tfit->function->reducer(subject, tfit->typeArguments, tfit->packArguments, ctx);
handleTypeFunctionReduction(subject, std::move(result));
}
}
void stepPack()
{
TypePackId subject = follow(queuedTps.front());
queuedTps.pop_front();
if (irreducible.contains(subject))
return;
if (FFlag::DebugLuauLogTypeFamilies)
printf("Trying to reduce %s\n", toString(subject, {true}).c_str());
if (const TypeFunctionInstanceTypePack* tfit = get<TypeFunctionInstanceTypePack>(subject))
{
if (!testParameters(subject, tfit))
return;
if (tryGuessing(subject))
return;
TypeFunctionReductionResult<TypePackId> result = tfit->function->reducer(subject, tfit->typeArguments, tfit->packArguments, ctx);
handleTypeFunctionReduction(subject, std::move(result));
}
}
void step()
{
if (!queuedTys.empty())
stepType();
else if (!queuedTps.empty())
stepPack();
}
};
static FunctionGraphReductionResult reduceFunctionsInternal(
VecDeque<TypeId> queuedTys,
VecDeque<TypePackId> queuedTps,
TypeOrTypePackIdSet shouldGuess,
std::vector<TypeId> cyclics,
Location location,
NotNull<TypeFunctionContext> ctx,
bool force
)
{
TypeFunctionReducer reducer{std::move(queuedTys), std::move(queuedTps), std::move(shouldGuess), std::move(cyclics), location, ctx, force};
int iterationCount = 0;
// If we are reducing a type function while reducing a type function,
// we're probably doing something clowny. One known place this can
// occur is type function reduction => overload selection => subtyping
// => back to type function reduction. At worst, if there's a reduction
// that _doesn't_ loop forever and _needs_ reentrancy, we'll fail to
// handle that and potentially emit an error when we didn't need to.
if (ctx->normalizer->sharedState->reentrantTypeReduction)
return {};
TypeReductionRentrancyGuard _{ctx->normalizer->sharedState};
while (!reducer.done())
{
reducer.step();
++iterationCount;
if (iterationCount > DFInt::LuauTypeFamilyGraphReductionMaximumSteps)
{
reducer.result.errors.emplace_back(location, CodeTooComplex{});
break;
}
}
return std::move(reducer.result);
}
FunctionGraphReductionResult reduceTypeFunctions(TypeId entrypoint, Location location, NotNull<TypeFunctionContext> ctx, bool force)
{
InstanceCollector collector;
try
{
collector.traverse(entrypoint);
}
catch (RecursionLimitException&)
{
return FunctionGraphReductionResult{};
}
if (collector.tys.empty() && collector.tps.empty())
return {};
return reduceFunctionsInternal(
std::move(collector.tys),
std::move(collector.tps),
std::move(collector.shouldGuess),
std::move(collector.cyclicInstance),
location,
ctx,
force
);
}
FunctionGraphReductionResult reduceTypeFunctions(TypePackId entrypoint, Location location, NotNull<TypeFunctionContext> ctx, bool force)
{
InstanceCollector collector;
try
{
collector.traverse(entrypoint);
}
catch (RecursionLimitException&)
{
return FunctionGraphReductionResult{};
}
if (collector.tys.empty() && collector.tps.empty())
return {};
return reduceFunctionsInternal(
std::move(collector.tys),
std::move(collector.tps),
std::move(collector.shouldGuess),
std::move(collector.cyclicInstance),
location,
ctx,
force
);
}
bool isPending(TypeId ty, ConstraintSolver* solver)
{
if (FFlag::LuauEagerGeneralization4)
{
if (auto tfit = get<TypeFunctionInstanceType>(ty); tfit && tfit->state == TypeFunctionInstanceState::Unsolved)
return true;
return is<BlockedType, PendingExpansionType>(ty) || (solver && solver->hasUnresolvedConstraints(ty));
}
else
return is<BlockedType, PendingExpansionType, TypeFunctionInstanceType>(ty) || (solver && solver->hasUnresolvedConstraints(ty));
}
} // namespace Luau