mirror of
https://github.com/luau-lang/luau.git
synced 2024-11-15 06:15:44 +08:00
Sync to upstream/release/611 (#1160)
# What's changed? ### Native Code Generation * Fixed an UAF relating to reusing a hash key after a weak table has undergone some GC. * Fixed a bounds check on arm64 to allow access to the last byte of a buffer. ### New Type Solver * Type states now preserves error-suppression, i.e. `local x: any = 5` and `x.foo` does not error. * Made error-suppression logic in subtyping more accurate. * Subtyping now knows how to reduce type families. * Fixed function call overload resolution so that the return type resolves to the correct overload. * Fixed a case where we attempted to reduce irreducible type families a few too many times, leading to duplicate errors. * Type checker needs to type check annotations in function signatures to be able to report errors relating to those annotations. * Fixed an UAF from a pointer to stack-allocated data in Subtyping's `explainReasonings`. ### Nonstrict Type Checker * Fixed a crash when calling a checked function of the form `math.abs` with an incorrect argument type. * Fixed a crash when calling a checked function with a number of arguments that did not exactly match the number of parameters required. --- ### Internal Contributors Co-authored-by: Aaron Weiss <aaronweiss@roblox.com> Co-authored-by: Andy Friesen <afriesen@roblox.com> Co-authored-by: Vyacheslav Egorov <vegorov@roblox.com> --------- Co-authored-by: Aaron Weiss <aaronweiss@roblox.com> Co-authored-by: Andy Friesen <afriesen@roblox.com> Co-authored-by: Vighnesh <vvijay@roblox.com> Co-authored-by: Aviral Goel <agoel@roblox.com> Co-authored-by: David Cope <dcope@roblox.com> Co-authored-by: Lily Brown <lbrown@roblox.com> Co-authored-by: Vyacheslav Egorov <vegorov@roblox.com>
This commit is contained in:
parent
974963a870
commit
67ce75e870
@ -143,6 +143,18 @@ private:
|
||||
*/
|
||||
TypePackId freshTypePack(const ScopePtr& scope);
|
||||
|
||||
/**
|
||||
* Allocate a new TypePack with the given head and tail.
|
||||
*
|
||||
* Avoids allocating 0-length type packs:
|
||||
*
|
||||
* If the head is non-empty, allocate and return a type pack with the given
|
||||
* head and tail.
|
||||
* If the head is empty and tail is non-empty, return *tail.
|
||||
* If both the head and tail are empty, return an empty type pack.
|
||||
*/
|
||||
TypePackId addTypePack(std::vector<TypeId> head, std::optional<TypePackId> tail);
|
||||
|
||||
/**
|
||||
* Fabricates a scope that is a child of another scope.
|
||||
* @param node the lexical node that the scope belongs to.
|
||||
|
@ -254,7 +254,6 @@ struct ConstraintSolver
|
||||
bool hasUnresolvedConstraints(TypeId ty);
|
||||
|
||||
private:
|
||||
|
||||
/** Helper used by tryDispatch(SubtypeConstraint) and
|
||||
* tryDispatch(PackSubtypeConstraint)
|
||||
*
|
||||
@ -265,7 +264,7 @@ private:
|
||||
*
|
||||
* If unification succeeds, unblock every type changed by the unification.
|
||||
*/
|
||||
template <typename TID>
|
||||
template<typename TID>
|
||||
bool tryUnify(NotNull<const Constraint> constraint, TID subTy, TID superTy);
|
||||
|
||||
/**
|
||||
|
@ -380,13 +380,22 @@ struct NonStrictFunctionDefinitionError
|
||||
bool operator==(const NonStrictFunctionDefinitionError& rhs) const;
|
||||
};
|
||||
|
||||
using TypeErrorData = Variant<TypeMismatch, UnknownSymbol, UnknownProperty, NotATable, CannotExtendTable, OnlyTablesCanHaveMethods,
|
||||
DuplicateTypeDefinition, CountMismatch, FunctionDoesNotTakeSelf, FunctionRequiresSelf, OccursCheckFailed, UnknownRequire,
|
||||
IncorrectGenericParameterCount, SyntaxError, CodeTooComplex, UnificationTooComplex, UnknownPropButFoundLikeProp, GenericError, InternalError,
|
||||
CannotCallNonFunction, ExtraInformation, DeprecatedApiUsed, ModuleHasCyclicDependency, IllegalRequire, FunctionExitsWithoutReturning,
|
||||
DuplicateGenericParameter, CannotInferBinaryOperation, MissingProperties, SwappedGenericTypeParameter, OptionalValueAccess, MissingUnionProperty,
|
||||
TypesAreUnrelated, NormalizationTooComplex, TypePackMismatch, DynamicPropertyLookupOnClassesUnsafe, UninhabitedTypeFamily,
|
||||
UninhabitedTypePackFamily, WhereClauseNeeded, PackWhereClauseNeeded, CheckedFunctionCallError, NonStrictFunctionDefinitionError>;
|
||||
struct CheckedFunctionIncorrectArgs
|
||||
{
|
||||
std::string functionName;
|
||||
size_t expected;
|
||||
size_t actual;
|
||||
bool operator==(const CheckedFunctionIncorrectArgs& rhs) const;
|
||||
};
|
||||
|
||||
using TypeErrorData =
|
||||
Variant<TypeMismatch, UnknownSymbol, UnknownProperty, NotATable, CannotExtendTable, OnlyTablesCanHaveMethods, DuplicateTypeDefinition,
|
||||
CountMismatch, FunctionDoesNotTakeSelf, FunctionRequiresSelf, OccursCheckFailed, UnknownRequire, IncorrectGenericParameterCount, SyntaxError,
|
||||
CodeTooComplex, UnificationTooComplex, UnknownPropButFoundLikeProp, GenericError, InternalError, CannotCallNonFunction, ExtraInformation,
|
||||
DeprecatedApiUsed, ModuleHasCyclicDependency, IllegalRequire, FunctionExitsWithoutReturning, DuplicateGenericParameter,
|
||||
CannotInferBinaryOperation, MissingProperties, SwappedGenericTypeParameter, OptionalValueAccess, MissingUnionProperty, TypesAreUnrelated,
|
||||
NormalizationTooComplex, TypePackMismatch, DynamicPropertyLookupOnClassesUnsafe, UninhabitedTypeFamily, UninhabitedTypePackFamily,
|
||||
WhereClauseNeeded, PackWhereClauseNeeded, CheckedFunctionCallError, NonStrictFunctionDefinitionError, CheckedFunctionIncorrectArgs>;
|
||||
|
||||
struct TypeErrorSummary
|
||||
{
|
||||
|
70
Analysis/include/Luau/OverloadResolution.h
Normal file
70
Analysis/include/Luau/OverloadResolution.h
Normal file
@ -0,0 +1,70 @@
|
||||
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
|
||||
#pragma once
|
||||
|
||||
#include "Luau/Ast.h"
|
||||
#include "Luau/InsertionOrderedMap.h"
|
||||
#include "Luau/NotNull.h"
|
||||
#include "Luau/TypeFwd.h"
|
||||
#include "Luau/Location.h"
|
||||
#include "Luau/Error.h"
|
||||
#include "Luau/Subtyping.h"
|
||||
|
||||
namespace Luau
|
||||
{
|
||||
|
||||
struct BuiltinTypes;
|
||||
struct TypeArena;
|
||||
struct Scope;
|
||||
struct InternalErrorReporter;
|
||||
struct TypeCheckLimits;
|
||||
struct Subtyping;
|
||||
|
||||
class Normalizer;
|
||||
|
||||
struct OverloadResolver
|
||||
{
|
||||
enum Analysis
|
||||
{
|
||||
Ok,
|
||||
TypeIsNotAFunction,
|
||||
ArityMismatch,
|
||||
OverloadIsNonviable, // Arguments were incompatible with the overloads parameters but were otherwise compatible by arity
|
||||
};
|
||||
|
||||
OverloadResolver(NotNull<BuiltinTypes> builtinTypes, NotNull<TypeArena> arena, NotNull<Normalizer> normalizer, NotNull<Scope> scope,
|
||||
NotNull<InternalErrorReporter> reporter, NotNull<TypeCheckLimits> limits, Location callLocation);
|
||||
|
||||
NotNull<BuiltinTypes> builtinTypes;
|
||||
NotNull<TypeArena> arena;
|
||||
NotNull<Normalizer> normalizer;
|
||||
NotNull<Scope> scope;
|
||||
NotNull<InternalErrorReporter> ice;
|
||||
NotNull<TypeCheckLimits> limits;
|
||||
Subtyping subtyping;
|
||||
Location callLoc;
|
||||
|
||||
// Resolver results
|
||||
std::vector<TypeId> ok;
|
||||
std::vector<TypeId> nonFunctions;
|
||||
std::vector<std::pair<TypeId, ErrorVec>> arityMismatches;
|
||||
std::vector<std::pair<TypeId, ErrorVec>> nonviableOverloads;
|
||||
InsertionOrderedMap<TypeId, std::pair<OverloadResolver::Analysis, size_t>> resolution;
|
||||
|
||||
|
||||
std::pair<OverloadResolver::Analysis, TypeId> selectOverload(TypeId ty, TypePackId args);
|
||||
void resolve(TypeId fnTy, const TypePack* args, AstExpr* selfExpr, const std::vector<AstExpr*>* argExprs);
|
||||
|
||||
private:
|
||||
std::optional<ErrorVec> testIsSubtype(const Location& location, TypeId subTy, TypeId superTy);
|
||||
std::optional<ErrorVec> testIsSubtype(const Location& location, TypePackId subTy, TypePackId superTy);
|
||||
std::pair<Analysis, ErrorVec> checkOverload(
|
||||
TypeId fnTy, const TypePack* args, AstExpr* fnLoc, const std::vector<AstExpr*>* argExprs, bool callMetamethodOk = true);
|
||||
static bool isLiteral(AstExpr* expr);
|
||||
LUAU_NOINLINE
|
||||
std::pair<Analysis, ErrorVec> checkOverload_(
|
||||
TypeId fnTy, const FunctionType* fn, const TypePack* args, AstExpr* fnExpr, const std::vector<AstExpr*>* argExprs);
|
||||
size_t indexof(Analysis analysis);
|
||||
void add(Analysis analysis, TypeId ty, ErrorVec&& errors);
|
||||
};
|
||||
|
||||
} // namespace Luau
|
@ -45,6 +45,8 @@ struct Scope
|
||||
|
||||
TypeLevel level;
|
||||
|
||||
Location location; // the spanning location associated with this scope
|
||||
|
||||
std::unordered_map<Name, TypeFun> exportedTypeBindings;
|
||||
std::unordered_map<Name, TypeFun> privateTypeBindings;
|
||||
std::unordered_map<Name, Location> typeAliasLocations;
|
||||
|
@ -5,6 +5,8 @@
|
||||
#include "Luau/TypeFwd.h"
|
||||
#include "Luau/TypePairHash.h"
|
||||
#include "Luau/TypePath.h"
|
||||
#include "Luau/TypeFamily.h"
|
||||
#include "Luau/TypeCheckLimits.h"
|
||||
#include "Luau/DenseHash.h"
|
||||
|
||||
#include <vector>
|
||||
@ -24,6 +26,7 @@ struct NormalizedClassType;
|
||||
struct NormalizedStringType;
|
||||
struct NormalizedFunctionType;
|
||||
struct TypeArena;
|
||||
struct TypeCheckLimits;
|
||||
struct Scope;
|
||||
struct TableIndexer;
|
||||
|
||||
@ -60,7 +63,6 @@ static const SubtypingReasoning kEmptyReasoning = SubtypingReasoning{TypePath::k
|
||||
struct SubtypingResult
|
||||
{
|
||||
bool isSubtype = false;
|
||||
bool isErrorSuppressing = false;
|
||||
bool normalizationTooComplex = false;
|
||||
bool isCacheable = true;
|
||||
|
||||
@ -109,6 +111,7 @@ struct Subtyping
|
||||
NotNull<InternalErrorReporter> iceReporter;
|
||||
|
||||
NotNull<Scope> scope;
|
||||
TypeCheckLimits limits;
|
||||
|
||||
enum class Variance
|
||||
{
|
||||
@ -199,6 +202,8 @@ private:
|
||||
SubtypingResult isCovariantWith(SubtypingEnvironment& env, const TypeIds& subTypes, const TypeIds& superTypes);
|
||||
|
||||
SubtypingResult isCovariantWith(SubtypingEnvironment& env, const VariadicTypePack* subVariadic, const VariadicTypePack* superVariadic);
|
||||
SubtypingResult isCovariantWith(SubtypingEnvironment& env, const TypeFamilyInstanceType* subFamilyInstance, const TypeId superTy);
|
||||
SubtypingResult isCovariantWith(SubtypingEnvironment& env, const TypeId subTy, const TypeFamilyInstanceType* superFamilyInstance);
|
||||
|
||||
bool bindGeneric(SubtypingEnvironment& env, TypeId subTp, TypeId superTp);
|
||||
bool bindGeneric(SubtypingEnvironment& env, TypePackId subTp, TypePackId superTp);
|
||||
@ -206,6 +211,21 @@ private:
|
||||
template<typename T, typename Container>
|
||||
TypeId makeAggregateType(const Container& container, TypeId orElse);
|
||||
|
||||
template<typename T>
|
||||
T handleTypeFamilyReductionResult(const TypeFamilyInstanceType* tf)
|
||||
{
|
||||
TypeFamilyContext context{arena, builtinTypes, scope, normalizer, iceReporter, NotNull{&limits}};
|
||||
TypeFamilyReductionResult<TypeId> result = tf->family->reducer(tf->typeArguments, tf->packArguments, NotNull{&context});
|
||||
if (!result.blockedTypes.empty())
|
||||
unexpected(result.blockedTypes[0]);
|
||||
else if (!result.blockedPacks.empty())
|
||||
unexpected(result.blockedPacks[0]);
|
||||
else if (result.uninhabited || result.result == std::nullopt)
|
||||
return builtinTypes->neverType;
|
||||
return *result.result;
|
||||
}
|
||||
|
||||
[[noreturn]] void unexpected(TypeId ty);
|
||||
[[noreturn]] void unexpected(TypePackId tp);
|
||||
};
|
||||
|
||||
|
@ -56,11 +56,33 @@ std::vector<TypeId> reduceUnion(const std::vector<TypeId>& types);
|
||||
*/
|
||||
TypeId stripNil(NotNull<BuiltinTypes> builtinTypes, TypeArena& arena, TypeId ty);
|
||||
|
||||
enum class ErrorSuppression
|
||||
struct ErrorSuppression
|
||||
{
|
||||
Suppress,
|
||||
DoNotSuppress,
|
||||
NormalizationFailed
|
||||
enum Value
|
||||
{
|
||||
Suppress,
|
||||
DoNotSuppress,
|
||||
NormalizationFailed,
|
||||
};
|
||||
|
||||
ErrorSuppression() = default;
|
||||
constexpr ErrorSuppression(Value enumValue) : value(enumValue) { }
|
||||
|
||||
constexpr operator Value() const { return value; }
|
||||
explicit operator bool() const = delete;
|
||||
|
||||
ErrorSuppression orElse(const ErrorSuppression& other) const
|
||||
{
|
||||
switch (value)
|
||||
{
|
||||
case DoNotSuppress:
|
||||
return other;
|
||||
default:
|
||||
return *this;
|
||||
}
|
||||
}
|
||||
private:
|
||||
Value value;
|
||||
};
|
||||
|
||||
/**
|
||||
@ -118,6 +140,8 @@ struct TryPair
|
||||
template<typename A, typename B, typename Ty>
|
||||
TryPair<const A*, const B*> get2(Ty one, Ty two)
|
||||
{
|
||||
static_assert(std::is_pointer_v<Ty>, "argument must be a pointer type");
|
||||
|
||||
const A* a = get<A>(one);
|
||||
const B* b = get<B>(two);
|
||||
if (a && b)
|
||||
|
@ -167,6 +167,7 @@ void ConstraintGenerator::visitModuleRoot(AstStatBlock* block)
|
||||
ScopePtr scope = std::make_shared<Scope>(globalScope);
|
||||
rootScope = scope.get();
|
||||
scopes.emplace_back(block->location, scope);
|
||||
rootScope->location = block->location;
|
||||
module->astScopes[block] = NotNull{scope.get()};
|
||||
|
||||
rootScope->returnType = freshTypePack(scope);
|
||||
@ -192,10 +193,24 @@ TypePackId ConstraintGenerator::freshTypePack(const ScopePtr& scope)
|
||||
return arena->addTypePack(TypePackVar{std::move(f)});
|
||||
}
|
||||
|
||||
TypePackId ConstraintGenerator::addTypePack(std::vector<TypeId> head, std::optional<TypePackId> tail)
|
||||
{
|
||||
if (head.empty())
|
||||
{
|
||||
if (tail)
|
||||
return *tail;
|
||||
else
|
||||
return builtinTypes->emptyTypePack;
|
||||
}
|
||||
else
|
||||
return arena->addTypePack(TypePack{std::move(head), tail});
|
||||
}
|
||||
|
||||
ScopePtr ConstraintGenerator::childScope(AstNode* node, const ScopePtr& parent)
|
||||
{
|
||||
auto scope = std::make_shared<Scope>(parent);
|
||||
scopes.emplace_back(node->location, scope);
|
||||
scope->location = node->location;
|
||||
|
||||
scope->returnType = parent->returnType;
|
||||
scope->varargPack = parent->varargPack;
|
||||
@ -1278,7 +1293,7 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatDeclareClas
|
||||
if (FunctionType* ftv = getMutable<FunctionType>(propTy))
|
||||
{
|
||||
ftv->argNames.insert(ftv->argNames.begin(), FunctionArgument{"self", {}});
|
||||
ftv->argTypes = arena->addTypePack(TypePack{{classTy}, ftv->argTypes});
|
||||
ftv->argTypes = addTypePack({classTy}, ftv->argTypes);
|
||||
|
||||
ftv->hasSelf = true;
|
||||
}
|
||||
@ -1407,10 +1422,7 @@ InferencePack ConstraintGenerator::checkPack(
|
||||
}
|
||||
}
|
||||
|
||||
if (head.empty() && tail)
|
||||
return InferencePack{*tail};
|
||||
else
|
||||
return InferencePack{arena->addTypePack(TypePack{std::move(head), tail})};
|
||||
return InferencePack{addTypePack(std::move(head), tail)};
|
||||
}
|
||||
|
||||
InferencePack ConstraintGenerator::checkPack(
|
||||
@ -1611,7 +1623,7 @@ InferencePack ConstraintGenerator::checkPack(const ScopePtr& scope, AstExprCall*
|
||||
|
||||
// TODO: How do expectedTypes play into this? Do they?
|
||||
TypePackId rets = arena->addTypePack(BlockedTypePack{});
|
||||
TypePackId argPack = arena->addTypePack(TypePack{args, argTail});
|
||||
TypePackId argPack = addTypePack(std::move(args), argTail);
|
||||
FunctionType ftv(TypeLevel{}, scope.get(), argPack, rets, std::nullopt, call->self);
|
||||
|
||||
NotNull<Constraint> fcc = addConstraint(scope, call->func->location,
|
||||
@ -2283,12 +2295,33 @@ std::optional<TypeId> ConstraintGenerator::checkLValue(const ScopePtr& scope, As
|
||||
{
|
||||
if (auto lt = getMutable<LocalType>(*ty))
|
||||
++lt->blockCount;
|
||||
else if (auto ut = getMutable<UnionType>(*ty))
|
||||
{
|
||||
for (TypeId optTy : ut->options)
|
||||
if (auto lt = getMutable<LocalType>(optTy))
|
||||
++lt->blockCount;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ty = arena->addType(LocalType{builtinTypes->neverType, /* blockCount */ 1, local->local->name.value});
|
||||
|
||||
if (annotatedTy)
|
||||
{
|
||||
switch (shouldSuppressErrors(normalizer, *annotatedTy))
|
||||
{
|
||||
case ErrorSuppression::DoNotSuppress:
|
||||
break;
|
||||
case ErrorSuppression::Suppress:
|
||||
ty = simplifyUnion(builtinTypes, arena, *ty, builtinTypes->errorType).result;
|
||||
break;
|
||||
case ErrorSuppression::NormalizationFailed:
|
||||
reportError(local->local->annotation->location, NormalizationTooComplex{});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
scope->lvalueTypes[defId] = *ty;
|
||||
}
|
||||
|
||||
@ -2546,6 +2579,22 @@ Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprTable* expr,
|
||||
|
||||
TypeId itemTy = check(scope, item.value, checkExpectedIndexResultType).ty;
|
||||
|
||||
// we should preserve error-suppressingness from the expected value type if we have one
|
||||
if (expectedValueType)
|
||||
{
|
||||
switch (shouldSuppressErrors(normalizer, *expectedValueType))
|
||||
{
|
||||
case ErrorSuppression::DoNotSuppress:
|
||||
break;
|
||||
case ErrorSuppression::Suppress:
|
||||
itemTy = simplifyUnion(builtinTypes, arena, itemTy, builtinTypes->errorType).result;
|
||||
break;
|
||||
case ErrorSuppression::NormalizationFailed:
|
||||
reportError(item.value->location, NormalizationTooComplex{});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (isIndexedResultType && !pinnedIndexResultType)
|
||||
pinnedIndexResultType = itemTy;
|
||||
|
||||
@ -3071,7 +3120,7 @@ TypePackId ConstraintGenerator::resolveTypePack(const ScopePtr& scope, const Ast
|
||||
tail = resolveTypePack(scope, list.tailType, inTypeArguments, replaceErrorWithFresh);
|
||||
}
|
||||
|
||||
return arena->addTypePack(TypePack{head, tail});
|
||||
return addTypePack(std::move(head), tail);
|
||||
}
|
||||
|
||||
std::vector<std::pair<Name, GenericTypeDefinition>> ConstraintGenerator::createGenerics(
|
||||
|
@ -8,6 +8,7 @@
|
||||
#include "Luau/Instantiation.h"
|
||||
#include "Luau/Location.h"
|
||||
#include "Luau/ModuleResolver.h"
|
||||
#include "Luau/OverloadResolution.h"
|
||||
#include "Luau/Quantify.h"
|
||||
#include "Luau/Simplify.h"
|
||||
#include "Luau/TimeTrace.h"
|
||||
@ -1098,10 +1099,18 @@ bool ConstraintSolver::tryDispatch(const FunctionCallConstraint& c, NotNull<cons
|
||||
*asMutable(follow(*ty)) = BoundType{builtinTypes->anyType};
|
||||
}
|
||||
|
||||
OverloadResolver resolver{
|
||||
builtinTypes, NotNull{arena}, normalizer, constraint->scope, NotNull{&iceReporter}, NotNull{&limits}, c.callSite->location};
|
||||
auto [status, overload] = resolver.selectOverload(fn, argsPack);
|
||||
TypeId overloadToUse = fn;
|
||||
if (status == OverloadResolver::Analysis::Ok)
|
||||
overloadToUse = overload;
|
||||
|
||||
|
||||
TypeId inferredTy = arena->addType(FunctionType{TypeLevel{}, constraint->scope.get(), argsPack, c.result});
|
||||
Unifier2 u2{NotNull{arena}, builtinTypes, constraint->scope, NotNull{&iceReporter}};
|
||||
|
||||
const bool occursCheckPassed = u2.unify(fn, inferredTy);
|
||||
const bool occursCheckPassed = u2.unify(overloadToUse, inferredTy);
|
||||
|
||||
for (const auto& [expanded, additions] : u2.expandedFreeTypes)
|
||||
{
|
||||
@ -1115,7 +1124,7 @@ bool ConstraintSolver::tryDispatch(const FunctionCallConstraint& c, NotNull<cons
|
||||
unblock(c.result, constraint->location);
|
||||
|
||||
InstantiationQueuer queuer{constraint->scope, constraint->location, this};
|
||||
queuer.traverse(fn);
|
||||
queuer.traverse(overloadToUse);
|
||||
queuer.traverse(inferredTy);
|
||||
|
||||
return true;
|
||||
@ -1482,6 +1491,41 @@ bool ConstraintSolver::tryDispatch(const UnpackConstraint& c, NotNull<const Cons
|
||||
auto resultIter = begin(resultPack);
|
||||
auto resultEnd = end(resultPack);
|
||||
|
||||
auto apply = [&](TypeId resultTy, TypeId srcTy) {
|
||||
if (auto lt = getMutable<LocalType>(resultTy); c.resultIsLValue && lt)
|
||||
{
|
||||
lt->domain = simplifyUnion(builtinTypes, arena, lt->domain, srcTy).result;
|
||||
LUAU_ASSERT(lt->blockCount > 0);
|
||||
--lt->blockCount;
|
||||
|
||||
LUAU_ASSERT(0 <= lt->blockCount);
|
||||
|
||||
if (0 == lt->blockCount)
|
||||
asMutable(resultTy)->ty.emplace<BoundType>(lt->domain);
|
||||
}
|
||||
else if (get<BlockedType>(resultTy))
|
||||
{
|
||||
if (follow(srcTy) == resultTy)
|
||||
{
|
||||
// It is sometimes the case that we find that a blocked type
|
||||
// is only blocked on itself. This doesn't actually
|
||||
// constitute any meaningful constraint, so we replace it
|
||||
// with a free type.
|
||||
TypeId f = freshType(arena, builtinTypes, constraint->scope);
|
||||
asMutable(resultTy)->ty.emplace<BoundType>(f);
|
||||
}
|
||||
else
|
||||
asMutable(resultTy)->ty.emplace<BoundType>(srcTy);
|
||||
}
|
||||
else
|
||||
{
|
||||
LUAU_ASSERT(c.resultIsLValue);
|
||||
unify(constraint->scope, constraint->location, resultTy, srcTy);
|
||||
}
|
||||
|
||||
unblock(resultTy, constraint->location);
|
||||
};
|
||||
|
||||
size_t i = 0;
|
||||
while (resultIter != resultEnd)
|
||||
{
|
||||
@ -1493,38 +1537,15 @@ bool ConstraintSolver::tryDispatch(const UnpackConstraint& c, NotNull<const Cons
|
||||
|
||||
if (resultTy)
|
||||
{
|
||||
if (auto lt = getMutable<LocalType>(resultTy); c.resultIsLValue && lt)
|
||||
// when we preserve the error-suppression of types through typestate,
|
||||
// we introduce a union with the error type, so we need to find the local type in those options to update.
|
||||
if (auto ut = getMutable<UnionType>(resultTy))
|
||||
{
|
||||
lt->domain = simplifyUnion(builtinTypes, arena, lt->domain, srcTy).result;
|
||||
LUAU_ASSERT(lt->blockCount > 0);
|
||||
--lt->blockCount;
|
||||
|
||||
LUAU_ASSERT(0 <= lt->blockCount);
|
||||
|
||||
if (0 == lt->blockCount)
|
||||
asMutable(resultTy)->ty.emplace<BoundType>(lt->domain);
|
||||
}
|
||||
else if (get<BlockedType>(resultTy))
|
||||
{
|
||||
if (follow(srcTy) == resultTy)
|
||||
{
|
||||
// It is sometimes the case that we find that a blocked type
|
||||
// is only blocked on itself. This doesn't actually
|
||||
// constitute any meaningful constraint, so we replace it
|
||||
// with a free type.
|
||||
TypeId f = freshType(arena, builtinTypes, constraint->scope);
|
||||
asMutable(resultTy)->ty.emplace<BoundType>(f);
|
||||
}
|
||||
else
|
||||
asMutable(resultTy)->ty.emplace<BoundType>(srcTy);
|
||||
for (auto opt : ut->options)
|
||||
apply(opt, srcTy);
|
||||
}
|
||||
else
|
||||
{
|
||||
LUAU_ASSERT(c.resultIsLValue);
|
||||
unify(constraint->scope, constraint->location, resultTy, srcTy);
|
||||
}
|
||||
|
||||
unblock(resultTy, constraint->location);
|
||||
apply(resultTy, srcTy);
|
||||
}
|
||||
else
|
||||
unify(constraint->scope, constraint->location, resultTy, srcTy);
|
||||
|
@ -10,6 +10,7 @@
|
||||
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <type_traits>
|
||||
|
||||
LUAU_FASTINTVARIABLE(LuauIndentTypeMismatchMaxTypeLength, 10)
|
||||
@ -539,6 +540,12 @@ struct ErrorConverter
|
||||
return "Argument " + e.argument + " with type '" + toString(e.argumentType) + "' in function '" + e.functionName +
|
||||
"' is used in a way that will run time error";
|
||||
}
|
||||
|
||||
std::string operator()(const CheckedFunctionIncorrectArgs& e) const
|
||||
{
|
||||
return "Checked Function " + e.functionName + " expects " + std::to_string(e.expected) + " arguments, but received " +
|
||||
std::to_string(e.actual);
|
||||
}
|
||||
};
|
||||
|
||||
struct InvalidNameChecker
|
||||
@ -872,6 +879,11 @@ bool NonStrictFunctionDefinitionError::operator==(const NonStrictFunctionDefinit
|
||||
return functionName == rhs.functionName && argument == rhs.argument && argumentType == rhs.argumentType;
|
||||
}
|
||||
|
||||
bool CheckedFunctionIncorrectArgs::operator==(const CheckedFunctionIncorrectArgs& rhs) const
|
||||
{
|
||||
return functionName == rhs.functionName && expected == rhs.expected && actual == rhs.actual;
|
||||
}
|
||||
|
||||
std::string toString(const TypeError& error)
|
||||
{
|
||||
return toString(error, TypeErrorToStringOptions{});
|
||||
@ -1047,6 +1059,9 @@ void copyError(T& e, TypeArena& destArena, CloneState& cloneState)
|
||||
{
|
||||
e.argumentType = clone(e.argumentType);
|
||||
}
|
||||
else if constexpr (std::is_same_v<T, CheckedFunctionIncorrectArgs>)
|
||||
{
|
||||
}
|
||||
else
|
||||
static_assert(always_false_v<T>, "Non-exhaustive type switch");
|
||||
}
|
||||
|
@ -35,7 +35,6 @@ LUAU_FASTFLAGVARIABLE(LuauKnowsTheDataModel3, false)
|
||||
LUAU_FASTFLAGVARIABLE(DebugLuauDeferredConstraintResolution, false)
|
||||
LUAU_FASTFLAGVARIABLE(DebugLuauLogSolverToJson, false)
|
||||
LUAU_FASTFLAGVARIABLE(DebugLuauReadWriteProperties, false)
|
||||
LUAU_FASTFLAGVARIABLE(LuauRethrowSingleModuleIce, false)
|
||||
LUAU_FASTFLAGVARIABLE(DebugLuauLogSolverToJsonFile, false)
|
||||
|
||||
namespace Luau
|
||||
@ -677,7 +676,7 @@ std::vector<ModuleName> Frontend::checkQueuedModules(std::optional<FrontendOptio
|
||||
sendItemTask(i);
|
||||
nextItems.clear();
|
||||
|
||||
if (FFlag::LuauRethrowSingleModuleIce && processing == 0)
|
||||
if (processing == 0)
|
||||
{
|
||||
// Typechecking might have been cancelled by user, don't return partial results
|
||||
if (cancelled)
|
||||
@ -690,23 +689,7 @@ std::vector<ModuleName> Frontend::checkQueuedModules(std::optional<FrontendOptio
|
||||
|
||||
// If we aren't done, but don't have anything processing, we hit a cycle
|
||||
if (remaining != 0 && processing == 0)
|
||||
{
|
||||
if (!FFlag::LuauRethrowSingleModuleIce)
|
||||
{
|
||||
// Typechecking might have been cancelled by user, don't return partial results
|
||||
if (cancelled)
|
||||
return {};
|
||||
|
||||
// We might have stopped because of a pending exception
|
||||
if (itemWithException)
|
||||
{
|
||||
recordItemResult(buildQueueItems[*itemWithException]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
sendCycleItemTask();
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<ModuleName> checkedModules;
|
||||
|
@ -207,6 +207,9 @@ static void errorToString(std::ostream& stream, const T& err)
|
||||
else if constexpr (std::is_same_v<T, NonStrictFunctionDefinitionError>)
|
||||
stream << "NonStrictFunctionDefinitionError { functionName = '" + err.functionName + "', argument = '" + err.argument +
|
||||
"', argumentType = '" + toString(err.argumentType) + "' }";
|
||||
else if constexpr (std::is_same_v<T, CheckedFunctionIncorrectArgs>)
|
||||
stream << "CheckedFunction { functionName = '" + err.functionName + ", expected = " + std::to_string(err.expected) +
|
||||
", actual = " + std::to_string(err.actual) + "}";
|
||||
else
|
||||
static_assert(always_false_v<T>, "Non-exhaustive type switch");
|
||||
}
|
||||
|
@ -12,6 +12,7 @@
|
||||
#include "Luau/TypeArena.h"
|
||||
#include "Luau/TypeFamily.h"
|
||||
#include "Luau/Def.h"
|
||||
#include "Luau/ToString.h"
|
||||
#include "Luau/TypeFwd.h"
|
||||
|
||||
#include <iostream>
|
||||
@ -140,7 +141,6 @@ private:
|
||||
}
|
||||
|
||||
std::unordered_map<const Def*, TypeId> context;
|
||||
|
||||
};
|
||||
|
||||
struct NonStrictTypeChecker
|
||||
@ -543,7 +543,14 @@ struct NonStrictTypeChecker
|
||||
}
|
||||
}
|
||||
// For a checked function, these gotta be the same size
|
||||
LUAU_ASSERT(call->args.size == argTypes.size());
|
||||
|
||||
std::string functionName = getFunctionNameAsString(*call->func).value_or("");
|
||||
if (call->args.size != argTypes.size())
|
||||
{
|
||||
reportError(CheckedFunctionIncorrectArgs{functionName, argTypes.size(), call->args.size}, call->location);
|
||||
return fresh;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < call->args.size; i++)
|
||||
{
|
||||
// For example, if the arg is "hi"
|
||||
@ -559,12 +566,11 @@ struct NonStrictTypeChecker
|
||||
}
|
||||
|
||||
// Populate the context and now iterate through each of the arguments to the call to find out if we satisfy the types
|
||||
AstName name = getIdentifier(call->func);
|
||||
for (size_t i = 0; i < call->args.size; i++)
|
||||
{
|
||||
AstExpr* arg = call->args.data[i];
|
||||
if (auto runTimeFailureType = willRunTimeError(arg, fresh))
|
||||
reportError(CheckedFunctionCallError{argTypes[i], *runTimeFailureType, name.value, i}, arg->location);
|
||||
reportError(CheckedFunctionCallError{argTypes[i], *runTimeFailureType, functionName, i}, arg->location);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
349
Analysis/src/OverloadResolution.cpp
Normal file
349
Analysis/src/OverloadResolution.cpp
Normal file
@ -0,0 +1,349 @@
|
||||
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
|
||||
#include "Luau/OverloadResolution.h"
|
||||
|
||||
#include "Luau/Subtyping.h"
|
||||
#include "Luau/TxnLog.h"
|
||||
#include "Luau/Type.h"
|
||||
#include "Luau/TypePack.h"
|
||||
#include "Luau/TypeUtils.h"
|
||||
#include "Luau/TypeFamily.h"
|
||||
|
||||
namespace Luau
|
||||
{
|
||||
|
||||
OverloadResolver::OverloadResolver(NotNull<BuiltinTypes> builtinTypes, NotNull<TypeArena> arena, NotNull<Normalizer> normalizer, NotNull<Scope> scope,
|
||||
NotNull<InternalErrorReporter> reporter, NotNull<TypeCheckLimits> limits, Location callLocation)
|
||||
: builtinTypes(builtinTypes)
|
||||
, arena(arena)
|
||||
, normalizer(normalizer)
|
||||
, scope(scope)
|
||||
, ice(reporter)
|
||||
, limits(limits)
|
||||
, subtyping({builtinTypes, arena, normalizer, ice, scope})
|
||||
, callLoc(callLocation)
|
||||
{
|
||||
}
|
||||
|
||||
std::pair<OverloadResolver::Analysis, TypeId> OverloadResolver::selectOverload(TypeId ty, TypePackId argsPack)
|
||||
{
|
||||
TypeId t = follow(ty);
|
||||
if (auto it = get<IntersectionType>(t))
|
||||
{
|
||||
for (TypeId component : it)
|
||||
{
|
||||
if (auto ftv = get<FunctionType>(component))
|
||||
{
|
||||
SubtypingResult r = subtyping.isSubtype(argsPack, ftv->argTypes);
|
||||
if (r.isSubtype)
|
||||
return {Analysis::Ok, component};
|
||||
}
|
||||
else
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return {Analysis::OverloadIsNonviable, ty};
|
||||
}
|
||||
|
||||
void OverloadResolver::resolve(TypeId fnTy, const TypePack* args, AstExpr* selfExpr, const std::vector<AstExpr*>* argExprs)
|
||||
{
|
||||
fnTy = follow(fnTy);
|
||||
|
||||
auto it = get<IntersectionType>(fnTy);
|
||||
if (!it)
|
||||
{
|
||||
auto [analysis, errors] = checkOverload(fnTy, args, selfExpr, argExprs);
|
||||
add(analysis, fnTy, std::move(errors));
|
||||
return;
|
||||
}
|
||||
|
||||
for (TypeId ty : it)
|
||||
{
|
||||
if (resolution.find(ty) != resolution.end())
|
||||
continue;
|
||||
|
||||
auto [analysis, errors] = checkOverload(ty, args, selfExpr, argExprs);
|
||||
add(analysis, ty, std::move(errors));
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<ErrorVec> OverloadResolver::testIsSubtype(const Location& location, TypeId subTy, TypeId superTy)
|
||||
{
|
||||
auto r = subtyping.isSubtype(subTy, superTy);
|
||||
ErrorVec errors;
|
||||
|
||||
if (r.normalizationTooComplex)
|
||||
errors.emplace_back(location, NormalizationTooComplex{});
|
||||
|
||||
if (!r.isSubtype)
|
||||
{
|
||||
switch (shouldSuppressErrors(normalizer, subTy).orElse(shouldSuppressErrors(normalizer, superTy)))
|
||||
{
|
||||
case ErrorSuppression::Suppress:
|
||||
break;
|
||||
case ErrorSuppression::NormalizationFailed:
|
||||
errors.emplace_back(location, NormalizationTooComplex{});
|
||||
// intentionally fallthrough here since we couldn't prove this was error-suppressing
|
||||
case ErrorSuppression::DoNotSuppress:
|
||||
errors.emplace_back(location, TypeMismatch{superTy, subTy});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.empty())
|
||||
return std::nullopt;
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
std::optional<ErrorVec> OverloadResolver::testIsSubtype(const Location& location, TypePackId subTy, TypePackId superTy)
|
||||
{
|
||||
auto r = subtyping.isSubtype(subTy, superTy);
|
||||
ErrorVec errors;
|
||||
|
||||
if (r.normalizationTooComplex)
|
||||
errors.emplace_back(location, NormalizationTooComplex{});
|
||||
|
||||
if (!r.isSubtype)
|
||||
{
|
||||
switch (shouldSuppressErrors(normalizer, subTy).orElse(shouldSuppressErrors(normalizer, superTy)))
|
||||
{
|
||||
case ErrorSuppression::Suppress:
|
||||
break;
|
||||
case ErrorSuppression::NormalizationFailed:
|
||||
errors.emplace_back(location, NormalizationTooComplex{});
|
||||
// intentionally fallthrough here since we couldn't prove this was error-suppressing
|
||||
case ErrorSuppression::DoNotSuppress:
|
||||
errors.emplace_back(location, TypePackMismatch{superTy, subTy});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.empty())
|
||||
return std::nullopt;
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
std::pair<OverloadResolver::Analysis, ErrorVec> OverloadResolver::checkOverload(
|
||||
TypeId fnTy, const TypePack* args, AstExpr* fnLoc, const std::vector<AstExpr*>* argExprs, bool callMetamethodOk)
|
||||
{
|
||||
fnTy = follow(fnTy);
|
||||
|
||||
ErrorVec discard;
|
||||
if (get<AnyType>(fnTy) || get<ErrorType>(fnTy) || get<NeverType>(fnTy))
|
||||
return {Ok, {}};
|
||||
else if (auto fn = get<FunctionType>(fnTy))
|
||||
return checkOverload_(fnTy, fn, args, fnLoc, argExprs); // Intentionally split to reduce the stack pressure of this function.
|
||||
else if (auto callMm = findMetatableEntry(builtinTypes, discard, fnTy, "__call", callLoc); callMm && callMetamethodOk)
|
||||
{
|
||||
// Calling a metamethod forwards the `fnTy` as self.
|
||||
TypePack withSelf = *args;
|
||||
withSelf.head.insert(withSelf.head.begin(), fnTy);
|
||||
|
||||
std::vector<AstExpr*> withSelfExprs = *argExprs;
|
||||
withSelfExprs.insert(withSelfExprs.begin(), fnLoc);
|
||||
|
||||
return checkOverload(*callMm, &withSelf, fnLoc, &withSelfExprs, /*callMetamethodOk=*/false);
|
||||
}
|
||||
else
|
||||
return {TypeIsNotAFunction, {}}; // Intentionally empty. We can just fabricate the type error later on.
|
||||
}
|
||||
|
||||
bool OverloadResolver::isLiteral(AstExpr* expr)
|
||||
{
|
||||
if (auto group = expr->as<AstExprGroup>())
|
||||
return isLiteral(group->expr);
|
||||
else if (auto assertion = expr->as<AstExprTypeAssertion>())
|
||||
return isLiteral(assertion->expr);
|
||||
|
||||
return expr->is<AstExprConstantNil>() || expr->is<AstExprConstantBool>() || expr->is<AstExprConstantNumber>() ||
|
||||
expr->is<AstExprConstantString>() || expr->is<AstExprFunction>() || expr->is<AstExprTable>();
|
||||
}
|
||||
|
||||
std::pair<OverloadResolver::Analysis, ErrorVec> OverloadResolver::checkOverload_(
|
||||
TypeId fnTy, const FunctionType* fn, const TypePack* args, AstExpr* fnExpr, const std::vector<AstExpr*>* argExprs)
|
||||
{
|
||||
FamilyGraphReductionResult result =
|
||||
reduceFamilies(fnTy, callLoc, TypeFamilyContext{arena, builtinTypes, scope, normalizer, ice, limits}, /*force=*/true);
|
||||
if (!result.errors.empty())
|
||||
return {OverloadIsNonviable, result.errors};
|
||||
|
||||
ErrorVec argumentErrors;
|
||||
|
||||
TypeId prospectiveFunction = arena->addType(FunctionType{arena->addTypePack(*args), builtinTypes->anyTypePack});
|
||||
SubtypingResult sr = subtyping.isSubtype(fnTy, prospectiveFunction);
|
||||
|
||||
if (sr.isSubtype)
|
||||
return {Analysis::Ok, {}};
|
||||
|
||||
if (1 == sr.reasoning.size())
|
||||
{
|
||||
const SubtypingReasoning& reason = *sr.reasoning.begin();
|
||||
|
||||
const TypePath::Path justArguments{TypePath::PackField::Arguments};
|
||||
|
||||
if (reason.subPath == justArguments && reason.superPath == justArguments)
|
||||
{
|
||||
// If the subtype test failed only due to an arity mismatch,
|
||||
// it is still possible that this function call is okay.
|
||||
// Subtype testing does not know anything about optional
|
||||
// function arguments.
|
||||
//
|
||||
// This can only happen if the actual function call has a
|
||||
// finite set of arguments which is too short for the
|
||||
// function being called. If all of those unsatisfied
|
||||
// function arguments are options, then this function call
|
||||
// is ok.
|
||||
|
||||
const size_t firstUnsatisfiedArgument = argExprs->size();
|
||||
const auto [requiredHead, _requiredTail] = flatten(fn->argTypes);
|
||||
|
||||
// If too many arguments were supplied, this overload
|
||||
// definitely does not match.
|
||||
if (args->head.size() > requiredHead.size())
|
||||
{
|
||||
auto [minParams, optMaxParams] = getParameterExtents(TxnLog::empty(), fn->argTypes);
|
||||
TypeError error{fnExpr->location, CountMismatch{minParams, optMaxParams, args->head.size(), CountMismatch::Arg, false}};
|
||||
|
||||
return {Analysis::ArityMismatch, {error}};
|
||||
}
|
||||
|
||||
// If any of the unsatisfied arguments are not supertypes of
|
||||
// nil, then this overload does not match.
|
||||
for (size_t i = firstUnsatisfiedArgument; i < requiredHead.size(); ++i)
|
||||
{
|
||||
if (!subtyping.isSubtype(builtinTypes->nilType, requiredHead[i]).isSubtype)
|
||||
{
|
||||
auto [minParams, optMaxParams] = getParameterExtents(TxnLog::empty(), fn->argTypes);
|
||||
TypeError error{fnExpr->location, CountMismatch{minParams, optMaxParams, args->head.size(), CountMismatch::Arg, false}};
|
||||
|
||||
return {Analysis::ArityMismatch, {error}};
|
||||
}
|
||||
}
|
||||
|
||||
return {Analysis::Ok, {}};
|
||||
}
|
||||
}
|
||||
|
||||
ErrorVec errors;
|
||||
|
||||
for (const SubtypingReasoning& reason : sr.reasoning)
|
||||
{
|
||||
/* The return type of our prospective function is always
|
||||
* any... so any subtype failures here can only arise from
|
||||
* argument type mismatches.
|
||||
*/
|
||||
|
||||
Location argLocation;
|
||||
|
||||
if (const Luau::TypePath::Index* pathIndexComponent = get_if<Luau::TypePath::Index>(&reason.superPath.components.at(1)))
|
||||
{
|
||||
size_t nthArgument = pathIndexComponent->index;
|
||||
argLocation = argExprs->at(nthArgument)->location;
|
||||
|
||||
std::optional<TypeId> failedSubTy = traverseForType(fnTy, reason.subPath, builtinTypes);
|
||||
std::optional<TypeId> failedSuperTy = traverseForType(prospectiveFunction, reason.superPath, builtinTypes);
|
||||
|
||||
if (failedSubTy && failedSuperTy)
|
||||
{
|
||||
|
||||
switch (shouldSuppressErrors(normalizer, *failedSubTy).orElse(shouldSuppressErrors(normalizer, *failedSuperTy)))
|
||||
{
|
||||
case ErrorSuppression::Suppress:
|
||||
break;
|
||||
case ErrorSuppression::NormalizationFailed:
|
||||
errors.emplace_back(argLocation, NormalizationTooComplex{});
|
||||
// intentionally fallthrough here since we couldn't prove this was error-suppressing
|
||||
case ErrorSuppression::DoNotSuppress:
|
||||
// TODO extract location from the SubtypingResult path and argExprs
|
||||
switch (reason.variance)
|
||||
{
|
||||
case SubtypingVariance::Covariant:
|
||||
case SubtypingVariance::Contravariant:
|
||||
errors.emplace_back(argLocation, TypeMismatch{*failedSubTy, *failedSuperTy, TypeMismatch::CovariantContext});
|
||||
break;
|
||||
case SubtypingVariance::Invariant:
|
||||
errors.emplace_back(argLocation, TypeMismatch{*failedSubTy, *failedSuperTy, TypeMismatch::InvariantContext});
|
||||
break;
|
||||
default:
|
||||
LUAU_ASSERT(0);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<TypePackId> failedSubPack = traverseForPack(fnTy, reason.subPath, builtinTypes);
|
||||
std::optional<TypePackId> failedSuperPack = traverseForPack(prospectiveFunction, reason.superPath, builtinTypes);
|
||||
|
||||
if (failedSubPack && failedSuperPack)
|
||||
{
|
||||
LUAU_ASSERT(!argExprs->empty());
|
||||
argLocation = argExprs->at(argExprs->size() - 1)->location;
|
||||
|
||||
// TODO extract location from the SubtypingResult path and argExprs
|
||||
switch (reason.variance)
|
||||
{
|
||||
case SubtypingVariance::Covariant:
|
||||
errors.emplace_back(argLocation, TypePackMismatch{*failedSubPack, *failedSuperPack});
|
||||
break;
|
||||
case SubtypingVariance::Contravariant:
|
||||
errors.emplace_back(argLocation, TypePackMismatch{*failedSuperPack, *failedSubPack});
|
||||
break;
|
||||
case SubtypingVariance::Invariant:
|
||||
errors.emplace_back(argLocation, TypePackMismatch{*failedSubPack, *failedSuperPack});
|
||||
break;
|
||||
default:
|
||||
LUAU_ASSERT(0);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {Analysis::OverloadIsNonviable, std::move(errors)};
|
||||
}
|
||||
|
||||
size_t OverloadResolver::indexof(Analysis analysis)
|
||||
{
|
||||
switch (analysis)
|
||||
{
|
||||
case Ok:
|
||||
return ok.size();
|
||||
case TypeIsNotAFunction:
|
||||
return nonFunctions.size();
|
||||
case ArityMismatch:
|
||||
return arityMismatches.size();
|
||||
case OverloadIsNonviable:
|
||||
return nonviableOverloads.size();
|
||||
}
|
||||
|
||||
ice->ice("Inexhaustive switch in FunctionCallResolver::indexof");
|
||||
}
|
||||
|
||||
void OverloadResolver::add(Analysis analysis, TypeId ty, ErrorVec&& errors)
|
||||
{
|
||||
resolution.insert(ty, {analysis, indexof(analysis)});
|
||||
|
||||
switch (analysis)
|
||||
{
|
||||
case Ok:
|
||||
LUAU_ASSERT(errors.empty());
|
||||
ok.push_back(ty);
|
||||
break;
|
||||
case TypeIsNotAFunction:
|
||||
LUAU_ASSERT(errors.empty());
|
||||
nonFunctions.push_back(ty);
|
||||
break;
|
||||
case ArityMismatch:
|
||||
LUAU_ASSERT(!errors.empty());
|
||||
arityMismatches.emplace_back(ty, std::move(errors));
|
||||
break;
|
||||
case OverloadIsNonviable:
|
||||
nonviableOverloads.emplace_back(ty, std::move(errors));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
} // namespace Luau
|
@ -10,6 +10,8 @@
|
||||
#include "Luau/ToString.h"
|
||||
#include "Luau/Type.h"
|
||||
#include "Luau/TypeArena.h"
|
||||
#include "Luau/TypeCheckLimits.h"
|
||||
#include "Luau/TypeFamily.h"
|
||||
#include "Luau/TypePack.h"
|
||||
#include "Luau/TypePath.h"
|
||||
#include "Luau/TypeUtils.h"
|
||||
@ -122,8 +124,6 @@ SubtypingResult& SubtypingResult::andAlso(const SubtypingResult& other)
|
||||
reasoning = mergeReasonings(reasoning, other.reasoning);
|
||||
|
||||
isSubtype &= other.isSubtype;
|
||||
// `|=` is intentional here, we want to preserve error related flags.
|
||||
isErrorSuppressing |= other.isErrorSuppressing;
|
||||
normalizationTooComplex |= other.normalizationTooComplex;
|
||||
isCacheable &= other.isCacheable;
|
||||
|
||||
@ -145,7 +145,6 @@ SubtypingResult& SubtypingResult::orElse(const SubtypingResult& other)
|
||||
}
|
||||
|
||||
isSubtype |= other.isSubtype;
|
||||
isErrorSuppressing |= other.isErrorSuppressing;
|
||||
normalizationTooComplex |= other.normalizationTooComplex;
|
||||
isCacheable &= other.isCacheable;
|
||||
|
||||
@ -218,14 +217,13 @@ SubtypingResult SubtypingResult::negate(const SubtypingResult& result)
|
||||
{
|
||||
return SubtypingResult{
|
||||
!result.isSubtype,
|
||||
result.isErrorSuppressing,
|
||||
result.normalizationTooComplex,
|
||||
};
|
||||
}
|
||||
|
||||
SubtypingResult SubtypingResult::all(const std::vector<SubtypingResult>& results)
|
||||
{
|
||||
SubtypingResult acc{true, false};
|
||||
SubtypingResult acc{true};
|
||||
for (const SubtypingResult& current : results)
|
||||
acc.andAlso(current);
|
||||
return acc;
|
||||
@ -233,7 +231,7 @@ SubtypingResult SubtypingResult::all(const std::vector<SubtypingResult>& results
|
||||
|
||||
SubtypingResult SubtypingResult::any(const std::vector<SubtypingResult>& results)
|
||||
{
|
||||
SubtypingResult acc{false, false};
|
||||
SubtypingResult acc{false};
|
||||
for (const SubtypingResult& current : results)
|
||||
acc.orElse(current);
|
||||
return acc;
|
||||
@ -408,7 +406,7 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypeId sub
|
||||
else if (auto superUnion = get<UnionType>(superTy))
|
||||
{
|
||||
result = isCovariantWith(env, subTy, superUnion);
|
||||
if (!result.isSubtype && !result.isErrorSuppressing && !result.normalizationTooComplex)
|
||||
if (!result.isSubtype && !result.normalizationTooComplex)
|
||||
{
|
||||
SubtypingResult semantic = isCovariantWith(env, normalizer->normalize(subTy), normalizer->normalize(superTy));
|
||||
if (semantic.isSubtype)
|
||||
@ -423,7 +421,7 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypeId sub
|
||||
else if (auto subIntersection = get<IntersectionType>(subTy))
|
||||
{
|
||||
result = isCovariantWith(env, subIntersection, superTy);
|
||||
if (!result.isSubtype && !result.isErrorSuppressing && !result.normalizationTooComplex)
|
||||
if (!result.isSubtype && !result.normalizationTooComplex)
|
||||
{
|
||||
SubtypingResult semantic = isCovariantWith(env, normalizer->normalize(subTy), normalizer->normalize(superTy));
|
||||
if (semantic.isSubtype)
|
||||
@ -450,20 +448,20 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypeId sub
|
||||
LUAU_ASSERT(!get<IntersectionType>(subTy)); // TODO: replace with ice.
|
||||
|
||||
bool errorSuppressing = get<ErrorType>(subTy);
|
||||
result = {!errorSuppressing, errorSuppressing};
|
||||
result = {!errorSuppressing};
|
||||
}
|
||||
else if (get<NeverType>(subTy))
|
||||
result = {true};
|
||||
else if (get<ErrorType>(superTy))
|
||||
result = {false, true};
|
||||
result = {false};
|
||||
else if (get<ErrorType>(subTy))
|
||||
result = {false, true};
|
||||
result = {false};
|
||||
else if (auto p = get2<NegationType, NegationType>(subTy, superTy))
|
||||
result = isCovariantWith(env, p.first->ty, p.second->ty).withBothComponent(TypePath::TypeField::Negated);
|
||||
else if (auto subNegation = get<NegationType>(subTy))
|
||||
{
|
||||
result = isCovariantWith(env, subNegation, superTy);
|
||||
if (!result.isSubtype && !result.isErrorSuppressing && !result.normalizationTooComplex)
|
||||
if (!result.isSubtype && !result.normalizationTooComplex)
|
||||
{
|
||||
SubtypingResult semantic = isCovariantWith(env, normalizer->normalize(subTy), normalizer->normalize(superTy));
|
||||
if (semantic.isSubtype)
|
||||
@ -476,7 +474,7 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypeId sub
|
||||
else if (auto superNegation = get<NegationType>(superTy))
|
||||
{
|
||||
result = isCovariantWith(env, subTy, superNegation);
|
||||
if (!result.isSubtype && !result.isErrorSuppressing && !result.normalizationTooComplex)
|
||||
if (!result.isSubtype && !result.normalizationTooComplex)
|
||||
{
|
||||
SubtypingResult semantic = isCovariantWith(env, normalizer->normalize(subTy), normalizer->normalize(superTy));
|
||||
if (semantic.isSubtype)
|
||||
@ -486,6 +484,10 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypeId sub
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (auto subTypeFamilyInstance = get<TypeFamilyInstanceType>(subTy))
|
||||
result = isCovariantWith(env, subTypeFamilyInstance, superTy);
|
||||
else if (auto superTypeFamilyInstance = get<TypeFamilyInstanceType>(superTy))
|
||||
result = isCovariantWith(env, subTy, superTypeFamilyInstance);
|
||||
else if (auto subGeneric = get<GenericType>(subTy); subGeneric && variance == Variance::Covariant)
|
||||
{
|
||||
bool ok = bindGeneric(env, subTy, superTy);
|
||||
@ -1256,7 +1258,7 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const Tabl
|
||||
SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const NormalizedType* subNorm, const NormalizedType* superNorm)
|
||||
{
|
||||
if (!subNorm || !superNorm)
|
||||
return {false, true, true};
|
||||
return {false, true};
|
||||
|
||||
SubtypingResult result = isCovariantWith(env, subNorm->tops, superNorm->tops);
|
||||
result.andAlso(isCovariantWith(env, subNorm->booleans, superNorm->booleans));
|
||||
@ -1412,6 +1414,20 @@ bool Subtyping::bindGeneric(SubtypingEnvironment& env, TypeId subTy, TypeId supe
|
||||
return true;
|
||||
}
|
||||
|
||||
SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const TypeFamilyInstanceType* subFamilyInstance, const TypeId superTy)
|
||||
{
|
||||
// Reduce the typefamily instance
|
||||
TypeId reduced = handleTypeFamilyReductionResult<TypeId>(subFamilyInstance);
|
||||
return isCovariantWith(env, reduced, superTy);
|
||||
}
|
||||
|
||||
SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const TypeId subTy, const TypeFamilyInstanceType* superFamilyInstance)
|
||||
{
|
||||
// Reduce the typefamily instance
|
||||
TypeId reduced = handleTypeFamilyReductionResult<TypeId>(superFamilyInstance);
|
||||
return isCovariantWith(env, subTy, reduced);
|
||||
}
|
||||
|
||||
/*
|
||||
* If, when performing a subtyping test, we encounter a generic on the left
|
||||
* side, it is permissible to tentatively bind that generic to the right side
|
||||
@ -1444,6 +1460,11 @@ TypeId Subtyping::makeAggregateType(const Container& container, TypeId orElse)
|
||||
return arena->addType(T{std::vector<TypeId>(begin(container), end(container))});
|
||||
}
|
||||
|
||||
void Subtyping::unexpected(TypeId ty)
|
||||
{
|
||||
iceReporter->ice(format("Unexpected type %s", toString(ty).c_str()));
|
||||
}
|
||||
|
||||
void Subtyping::unexpected(TypePackId tp)
|
||||
{
|
||||
iceReporter->ice(format("Unexpected type pack %s", toString(tp).c_str()));
|
||||
|
@ -11,6 +11,7 @@
|
||||
#include "Luau/Instantiation.h"
|
||||
#include "Luau/Metamethods.h"
|
||||
#include "Luau/Normalize.h"
|
||||
#include "Luau/OverloadResolution.h"
|
||||
#include "Luau/Subtyping.h"
|
||||
#include "Luau/ToString.h"
|
||||
#include "Luau/TxnLog.h"
|
||||
@ -240,7 +241,7 @@ struct TypeChecker2
|
||||
std::vector<NotNull<Scope>> stack;
|
||||
std::vector<TypeId> functionDeclStack;
|
||||
|
||||
DenseHashSet<TypeId> noTypeFamilyErrors{nullptr};
|
||||
DenseHashSet<TypeId> seenTypeFamilyInstances{nullptr};
|
||||
|
||||
Normalizer normalizer;
|
||||
Subtyping _subtyping;
|
||||
@ -377,7 +378,7 @@ struct TypeChecker2
|
||||
if (const AstStatExpr* stat = node->as<AstStatExpr>())
|
||||
{
|
||||
if (AstExprCall* call = stat->expr->as<AstExprCall>(); call && isErrorCall(call))
|
||||
return nullptr;
|
||||
return nullptr;
|
||||
|
||||
return stat;
|
||||
}
|
||||
@ -432,18 +433,16 @@ struct TypeChecker2
|
||||
|
||||
TypeId checkForFamilyInhabitance(TypeId instance, Location location)
|
||||
{
|
||||
if (noTypeFamilyErrors.find(instance))
|
||||
if (seenTypeFamilyInstances.find(instance))
|
||||
return instance;
|
||||
seenTypeFamilyInstances.insert(instance);
|
||||
|
||||
ErrorVec errors = reduceFamilies(
|
||||
instance, location, TypeFamilyContext{NotNull{&testArena}, builtinTypes, stack.back(), NotNull{&normalizer}, ice, limits}, true)
|
||||
.errors;
|
||||
|
||||
if (errors.empty())
|
||||
noTypeFamilyErrors.insert(instance);
|
||||
|
||||
if (!isErrorSuppressing(location, instance))
|
||||
reportErrors(std::move(errors));
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
@ -538,20 +537,21 @@ struct TypeChecker2
|
||||
Scope* findInnermostScope(Location location)
|
||||
{
|
||||
Scope* bestScope = module->getModuleScope().get();
|
||||
Location bestLocation = module->scopes[0].first;
|
||||
|
||||
for (size_t i = 0; i < module->scopes.size(); ++i)
|
||||
bool didNarrow;
|
||||
do
|
||||
{
|
||||
auto& [scopeBounds, scope] = module->scopes[i];
|
||||
if (scopeBounds.encloses(location))
|
||||
didNarrow = false;
|
||||
for (auto scope : bestScope->children)
|
||||
{
|
||||
if (scopeBounds.begin > bestLocation.begin || scopeBounds.end < bestLocation.end)
|
||||
if (scope->location.encloses(location))
|
||||
{
|
||||
bestScope = scope.get();
|
||||
bestLocation = scopeBounds;
|
||||
didNarrow = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} while (didNarrow && bestScope->children.size() > 0);
|
||||
|
||||
return bestScope;
|
||||
}
|
||||
@ -1192,7 +1192,7 @@ struct TypeChecker2
|
||||
TypeId expectedType = builtinTypes->nilType;
|
||||
|
||||
SubtypingResult r = subtyping->isSubtype(actualType, expectedType);
|
||||
LUAU_ASSERT(r.isSubtype || r.isErrorSuppressing);
|
||||
LUAU_ASSERT(r.isSubtype || isErrorSuppressing(expr->location, actualType));
|
||||
}
|
||||
|
||||
void visit(AstExprConstantBool* expr)
|
||||
@ -1201,7 +1201,7 @@ struct TypeChecker2
|
||||
TypeId expectedType = builtinTypes->booleanType;
|
||||
|
||||
SubtypingResult r = subtyping->isSubtype(actualType, expectedType);
|
||||
LUAU_ASSERT(r.isSubtype || r.isErrorSuppressing);
|
||||
LUAU_ASSERT(r.isSubtype || isErrorSuppressing(expr->location, actualType));
|
||||
}
|
||||
|
||||
void visit(AstExprConstantNumber* expr)
|
||||
@ -1210,7 +1210,7 @@ struct TypeChecker2
|
||||
TypeId expectedType = builtinTypes->numberType;
|
||||
|
||||
SubtypingResult r = subtyping->isSubtype(actualType, expectedType);
|
||||
LUAU_ASSERT(r.isSubtype || r.isErrorSuppressing);
|
||||
LUAU_ASSERT(r.isSubtype || isErrorSuppressing(expr->location, actualType));
|
||||
}
|
||||
|
||||
void visit(AstExprConstantString* expr)
|
||||
@ -1219,7 +1219,7 @@ struct TypeChecker2
|
||||
TypeId expectedType = builtinTypes->stringType;
|
||||
|
||||
SubtypingResult r = subtyping->isSubtype(actualType, expectedType);
|
||||
LUAU_ASSERT(r.isSubtype || r.isErrorSuppressing);
|
||||
LUAU_ASSERT(r.isSubtype || isErrorSuppressing(expr->location, actualType));
|
||||
}
|
||||
|
||||
void visit(AstExprLocal* expr)
|
||||
@ -1309,14 +1309,13 @@ struct TypeChecker2
|
||||
args.head.push_back(builtinTypes->anyType);
|
||||
}
|
||||
|
||||
FunctionCallResolver resolver{
|
||||
OverloadResolver resolver{
|
||||
builtinTypes,
|
||||
NotNull{&testArena},
|
||||
NotNull{&normalizer},
|
||||
NotNull{stack.back()},
|
||||
ice,
|
||||
limits,
|
||||
subtyping,
|
||||
call->location,
|
||||
};
|
||||
|
||||
@ -1369,7 +1368,7 @@ struct TypeChecker2
|
||||
{
|
||||
for (const auto& [ty, p] : resolver.resolution)
|
||||
{
|
||||
if (p.first == FunctionCallResolver::TypeIsNotAFunction)
|
||||
if (p.first == OverloadResolver::TypeIsNotAFunction)
|
||||
continue;
|
||||
|
||||
overloads.push_back(ty);
|
||||
@ -1393,305 +1392,6 @@ struct TypeChecker2
|
||||
}
|
||||
}
|
||||
|
||||
struct FunctionCallResolver
|
||||
{
|
||||
enum Analysis
|
||||
{
|
||||
Ok,
|
||||
TypeIsNotAFunction,
|
||||
ArityMismatch,
|
||||
OverloadIsNonviable, // Arguments were incompatible with the overload's parameters, but were otherwise compatible by arity.
|
||||
};
|
||||
|
||||
NotNull<BuiltinTypes> builtinTypes;
|
||||
NotNull<TypeArena> arena;
|
||||
NotNull<Normalizer> normalizer;
|
||||
NotNull<Scope> scope;
|
||||
NotNull<InternalErrorReporter> ice;
|
||||
NotNull<TypeCheckLimits> limits;
|
||||
NotNull<Subtyping> subtyping;
|
||||
Location callLoc;
|
||||
|
||||
std::vector<TypeId> ok;
|
||||
std::vector<TypeId> nonFunctions;
|
||||
std::vector<std::pair<TypeId, ErrorVec>> arityMismatches;
|
||||
std::vector<std::pair<TypeId, ErrorVec>> nonviableOverloads;
|
||||
InsertionOrderedMap<TypeId, std::pair<Analysis, size_t>> resolution;
|
||||
|
||||
private:
|
||||
std::optional<ErrorVec> testIsSubtype(const Location& location, TypeId subTy, TypeId superTy)
|
||||
{
|
||||
auto r = subtyping->isSubtype(subTy, superTy);
|
||||
ErrorVec errors;
|
||||
|
||||
if (r.normalizationTooComplex)
|
||||
errors.push_back(TypeError{location, NormalizationTooComplex{}});
|
||||
|
||||
if (!r.isSubtype && !r.isErrorSuppressing)
|
||||
errors.push_back(TypeError{location, TypeMismatch{superTy, subTy}});
|
||||
|
||||
if (errors.empty())
|
||||
return std::nullopt;
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
std::optional<ErrorVec> testIsSubtype(const Location& location, TypePackId subTy, TypePackId superTy)
|
||||
{
|
||||
auto r = subtyping->isSubtype(subTy, superTy);
|
||||
ErrorVec errors;
|
||||
|
||||
if (r.normalizationTooComplex)
|
||||
errors.push_back(TypeError{location, NormalizationTooComplex{}});
|
||||
|
||||
if (!r.isSubtype && !r.isErrorSuppressing)
|
||||
errors.push_back(TypeError{location, TypePackMismatch{superTy, subTy}});
|
||||
|
||||
if (errors.empty())
|
||||
return std::nullopt;
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
std::pair<Analysis, ErrorVec> checkOverload(
|
||||
TypeId fnTy, const TypePack* args, AstExpr* fnLoc, const std::vector<AstExpr*>* argExprs, bool callMetamethodOk = true)
|
||||
{
|
||||
fnTy = follow(fnTy);
|
||||
|
||||
ErrorVec discard;
|
||||
if (get<AnyType>(fnTy) || get<ErrorType>(fnTy) || get<NeverType>(fnTy))
|
||||
return {Ok, {}};
|
||||
else if (auto fn = get<FunctionType>(fnTy))
|
||||
return checkOverload_(fnTy, fn, args, fnLoc, argExprs); // Intentionally split to reduce the stack pressure of this function.
|
||||
else if (auto callMm = findMetatableEntry(builtinTypes, discard, fnTy, "__call", callLoc); callMm && callMetamethodOk)
|
||||
{
|
||||
// Calling a metamethod forwards the `fnTy` as self.
|
||||
TypePack withSelf = *args;
|
||||
withSelf.head.insert(withSelf.head.begin(), fnTy);
|
||||
|
||||
std::vector<AstExpr*> withSelfExprs = *argExprs;
|
||||
withSelfExprs.insert(withSelfExprs.begin(), fnLoc);
|
||||
|
||||
return checkOverload(*callMm, &withSelf, fnLoc, &withSelfExprs, /*callMetamethodOk=*/false);
|
||||
}
|
||||
else
|
||||
return {TypeIsNotAFunction, {}}; // Intentionally empty. We can just fabricate the type error later on.
|
||||
}
|
||||
|
||||
static bool isLiteral(AstExpr* expr)
|
||||
{
|
||||
if (auto group = expr->as<AstExprGroup>())
|
||||
return isLiteral(group->expr);
|
||||
else if (auto assertion = expr->as<AstExprTypeAssertion>())
|
||||
return isLiteral(assertion->expr);
|
||||
|
||||
return expr->is<AstExprConstantNil>() || expr->is<AstExprConstantBool>() || expr->is<AstExprConstantNumber>() ||
|
||||
expr->is<AstExprConstantString>() || expr->is<AstExprFunction>() || expr->is<AstExprTable>();
|
||||
}
|
||||
|
||||
LUAU_NOINLINE
|
||||
std::pair<Analysis, ErrorVec> checkOverload_(
|
||||
TypeId fnTy, const FunctionType* fn, const TypePack* args, AstExpr* fnExpr, const std::vector<AstExpr*>* argExprs)
|
||||
{
|
||||
FamilyGraphReductionResult result =
|
||||
reduceFamilies(fnTy, callLoc, TypeFamilyContext{arena, builtinTypes, scope, normalizer, ice, limits}, /*force=*/true);
|
||||
if (!result.errors.empty())
|
||||
return {OverloadIsNonviable, result.errors};
|
||||
|
||||
ErrorVec argumentErrors;
|
||||
|
||||
TypeId prospectiveFunction = arena->addType(FunctionType{arena->addTypePack(*args), builtinTypes->anyTypePack});
|
||||
SubtypingResult sr = subtyping->isSubtype(fnTy, prospectiveFunction);
|
||||
|
||||
if (sr.isSubtype)
|
||||
return {Analysis::Ok, {}};
|
||||
|
||||
if (1 == sr.reasoning.size())
|
||||
{
|
||||
const SubtypingReasoning& reason = *sr.reasoning.begin();
|
||||
|
||||
const TypePath::Path justArguments{TypePath::PackField::Arguments};
|
||||
|
||||
if (reason.subPath == justArguments && reason.superPath == justArguments)
|
||||
{
|
||||
// If the subtype test failed only due to an arity mismatch,
|
||||
// it is still possible that this function call is okay.
|
||||
// Subtype testing does not know anything about optional
|
||||
// function arguments.
|
||||
//
|
||||
// This can only happen if the actual function call has a
|
||||
// finite set of arguments which is too short for the
|
||||
// function being called. If all of those unsatisfied
|
||||
// function arguments are options, then this function call
|
||||
// is ok.
|
||||
|
||||
const size_t firstUnsatisfiedArgument = argExprs->size();
|
||||
const auto [requiredHead, _requiredTail] = flatten(fn->argTypes);
|
||||
|
||||
// If too many arguments were supplied, this overload
|
||||
// definitely does not match.
|
||||
if (args->head.size() > requiredHead.size())
|
||||
{
|
||||
auto [minParams, optMaxParams] = getParameterExtents(TxnLog::empty(), fn->argTypes);
|
||||
TypeError error{fnExpr->location, CountMismatch{minParams, optMaxParams, args->head.size(), CountMismatch::Arg, false}};
|
||||
|
||||
return {Analysis::ArityMismatch, {error}};
|
||||
}
|
||||
|
||||
// If any of the unsatisfied arguments are not supertypes of
|
||||
// nil, then this overload does not match.
|
||||
for (size_t i = firstUnsatisfiedArgument; i < requiredHead.size(); ++i)
|
||||
{
|
||||
if (!subtyping->isSubtype(builtinTypes->nilType, requiredHead[i]).isSubtype)
|
||||
{
|
||||
auto [minParams, optMaxParams] = getParameterExtents(TxnLog::empty(), fn->argTypes);
|
||||
TypeError error{fnExpr->location, CountMismatch{minParams, optMaxParams, args->head.size(), CountMismatch::Arg, false}};
|
||||
|
||||
return {Analysis::ArityMismatch, {error}};
|
||||
}
|
||||
}
|
||||
|
||||
return {Analysis::Ok, {}};
|
||||
}
|
||||
}
|
||||
|
||||
ErrorVec errors;
|
||||
|
||||
if (!sr.isErrorSuppressing)
|
||||
{
|
||||
for (const SubtypingReasoning& reason : sr.reasoning)
|
||||
{
|
||||
/* The return type of our prospective function is always
|
||||
* any... so any subtype failures here can only arise from
|
||||
* argument type mismatches.
|
||||
*/
|
||||
|
||||
Location argLocation;
|
||||
|
||||
if (const Luau::TypePath::Index* pathIndexComponent = get_if<Luau::TypePath::Index>(&reason.superPath.components.at(1)))
|
||||
{
|
||||
size_t nthArgument = pathIndexComponent->index;
|
||||
argLocation = argExprs->at(nthArgument)->location;
|
||||
|
||||
std::optional<TypeId> failedSubTy = traverseForType(fnTy, reason.subPath, builtinTypes);
|
||||
std::optional<TypeId> failedSuperTy = traverseForType(prospectiveFunction, reason.superPath, builtinTypes);
|
||||
|
||||
if (failedSubTy && failedSuperTy)
|
||||
{
|
||||
// TODO extract location from the SubtypingResult path and argExprs
|
||||
switch (reason.variance)
|
||||
{
|
||||
case SubtypingVariance::Covariant:
|
||||
case SubtypingVariance::Contravariant:
|
||||
errors.emplace_back(argLocation, TypeMismatch{*failedSubTy, *failedSuperTy, TypeMismatch::CovariantContext});
|
||||
break;
|
||||
case SubtypingVariance::Invariant:
|
||||
errors.emplace_back(argLocation, TypeMismatch{*failedSubTy, *failedSuperTy, TypeMismatch::InvariantContext});
|
||||
break;
|
||||
default:
|
||||
LUAU_ASSERT(0);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<TypePackId> failedSubPack = traverseForPack(fnTy, reason.subPath, builtinTypes);
|
||||
std::optional<TypePackId> failedSuperPack = traverseForPack(prospectiveFunction, reason.superPath, builtinTypes);
|
||||
|
||||
if (failedSubPack && failedSuperPack)
|
||||
{
|
||||
LUAU_ASSERT(!argExprs->empty());
|
||||
argLocation = argExprs->at(argExprs->size() - 1)->location;
|
||||
|
||||
// TODO extract location from the SubtypingResult path and argExprs
|
||||
switch (reason.variance)
|
||||
{
|
||||
case SubtypingVariance::Covariant:
|
||||
errors.emplace_back(argLocation, TypePackMismatch{*failedSubPack, *failedSuperPack});
|
||||
break;
|
||||
case SubtypingVariance::Contravariant:
|
||||
errors.emplace_back(argLocation, TypePackMismatch{*failedSuperPack, *failedSubPack});
|
||||
break;
|
||||
case SubtypingVariance::Invariant:
|
||||
errors.emplace_back(argLocation, TypePackMismatch{*failedSubPack, *failedSuperPack});
|
||||
break;
|
||||
default:
|
||||
LUAU_ASSERT(0);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return {Analysis::OverloadIsNonviable, std::move(errors)};
|
||||
}
|
||||
|
||||
size_t indexof(Analysis analysis)
|
||||
{
|
||||
switch (analysis)
|
||||
{
|
||||
case Ok:
|
||||
return ok.size();
|
||||
case TypeIsNotAFunction:
|
||||
return nonFunctions.size();
|
||||
case ArityMismatch:
|
||||
return arityMismatches.size();
|
||||
case OverloadIsNonviable:
|
||||
return nonviableOverloads.size();
|
||||
}
|
||||
|
||||
ice->ice("Inexhaustive switch in FunctionCallResolver::indexof");
|
||||
}
|
||||
|
||||
void add(Analysis analysis, TypeId ty, ErrorVec&& errors)
|
||||
{
|
||||
resolution.insert(ty, {analysis, indexof(analysis)});
|
||||
|
||||
switch (analysis)
|
||||
{
|
||||
case Ok:
|
||||
LUAU_ASSERT(errors.empty());
|
||||
ok.push_back(ty);
|
||||
break;
|
||||
case TypeIsNotAFunction:
|
||||
LUAU_ASSERT(errors.empty());
|
||||
nonFunctions.push_back(ty);
|
||||
break;
|
||||
case ArityMismatch:
|
||||
LUAU_ASSERT(!errors.empty());
|
||||
arityMismatches.emplace_back(ty, std::move(errors));
|
||||
break;
|
||||
case OverloadIsNonviable:
|
||||
nonviableOverloads.emplace_back(ty, std::move(errors));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
void resolve(TypeId fnTy, const TypePack* args, AstExpr* selfExpr, const std::vector<AstExpr*>* argExprs)
|
||||
{
|
||||
fnTy = follow(fnTy);
|
||||
|
||||
auto it = get<IntersectionType>(fnTy);
|
||||
if (!it)
|
||||
{
|
||||
auto [analysis, errors] = checkOverload(fnTy, args, selfExpr, argExprs);
|
||||
add(analysis, fnTy, std::move(errors));
|
||||
return;
|
||||
}
|
||||
|
||||
for (TypeId ty : it)
|
||||
{
|
||||
if (resolution.find(ty) != resolution.end())
|
||||
continue;
|
||||
|
||||
auto [analysis, errors] = checkOverload(ty, args, selfExpr, argExprs);
|
||||
add(analysis, ty, std::move(errors));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
void visit(AstExprCall* call)
|
||||
{
|
||||
visit(call->func, ValueContext::RValue);
|
||||
@ -1738,7 +1438,17 @@ struct TypeChecker2
|
||||
|
||||
if (std::optional<TypeId> strippedUnion = tryStripUnionFromNil(ty))
|
||||
{
|
||||
reportError(OptionalValueAccess{ty}, location);
|
||||
switch (shouldSuppressErrors(NotNull{&normalizer}, ty))
|
||||
{
|
||||
case ErrorSuppression::Suppress:
|
||||
break;
|
||||
case ErrorSuppression::NormalizationFailed:
|
||||
reportError(NormalizationTooComplex{}, location);
|
||||
// fallthrough intentional
|
||||
case ErrorSuppression::DoNotSuppress:
|
||||
reportError(OptionalValueAccess{ty}, location);
|
||||
}
|
||||
|
||||
return follow(*strippedUnion);
|
||||
}
|
||||
|
||||
@ -1784,7 +1494,18 @@ struct TypeChecker2
|
||||
else if (auto cls = get<ClassType>(exprType); cls && cls->indexer)
|
||||
testIsSubtype(indexType, cls->indexer->indexType, indexExpr->index->location);
|
||||
else if (get<UnionType>(exprType) && isOptional(exprType))
|
||||
reportError(OptionalValueAccess{exprType}, indexExpr->location);
|
||||
{
|
||||
switch (shouldSuppressErrors(NotNull{&normalizer}, exprType))
|
||||
{
|
||||
case ErrorSuppression::Suppress:
|
||||
break;
|
||||
case ErrorSuppression::NormalizationFailed:
|
||||
reportError(NormalizationTooComplex{}, indexExpr->location);
|
||||
// fallthrough intentional
|
||||
case ErrorSuppression::DoNotSuppress:
|
||||
reportError(OptionalValueAccess{exprType}, indexExpr->location);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void visit(AstExprFunction* fn)
|
||||
@ -1832,6 +1553,9 @@ struct TypeChecker2
|
||||
|
||||
if (arg->annotation)
|
||||
{
|
||||
// we need to typecheck any argument annotations themselves.
|
||||
visit(arg->annotation);
|
||||
|
||||
TypeId annotatedArgTy = lookupAnnotation(arg->annotation);
|
||||
|
||||
testIsSubtype(inferredArgTy, annotatedArgTy, arg->location);
|
||||
@ -1873,6 +1597,10 @@ struct TypeChecker2
|
||||
++argIt;
|
||||
}
|
||||
|
||||
// we need to typecheck the vararg annotation, if it exists.
|
||||
if (fn->vararg && fn->varargAnnotation)
|
||||
visit(fn->varargAnnotation);
|
||||
|
||||
bool reachesImplicitReturn = getFallthrough(fn->body) != nullptr;
|
||||
if (reachesImplicitReturn && !allowsNoReturnValues(follow(inferredFtv->retTypes)))
|
||||
reportError(FunctionExitsWithoutReturning{inferredFtv->retTypes}, getEndLocation(fn));
|
||||
@ -1880,6 +1608,10 @@ struct TypeChecker2
|
||||
|
||||
visit(fn->body);
|
||||
|
||||
// we need to typecheck the return annotation itself, if it exists.
|
||||
if (fn->returnAnnotation)
|
||||
visit(*fn->returnAnnotation);
|
||||
|
||||
functionDeclStack.pop_back();
|
||||
}
|
||||
|
||||
@ -2272,12 +2004,22 @@ struct TypeChecker2
|
||||
TypeId computedType = lookupType(expr->expr);
|
||||
|
||||
// Note: As an optimization, we try 'number <: number | string' first, as that is the more likely case.
|
||||
if (auto r = subtyping->isSubtype(annotationType, computedType); r.isSubtype || r.isErrorSuppressing)
|
||||
if (subtyping->isSubtype(annotationType, computedType).isSubtype)
|
||||
return;
|
||||
|
||||
if (auto r = subtyping->isSubtype(computedType, annotationType); r.isSubtype || r.isErrorSuppressing)
|
||||
if (subtyping->isSubtype(computedType, annotationType).isSubtype)
|
||||
return;
|
||||
|
||||
switch (shouldSuppressErrors(NotNull{&normalizer}, computedType).orElse(shouldSuppressErrors(NotNull{&normalizer}, annotationType)))
|
||||
{
|
||||
case ErrorSuppression::Suppress:
|
||||
return;
|
||||
case ErrorSuppression::NormalizationFailed:
|
||||
reportError(NormalizationTooComplex{}, expr->location);
|
||||
case ErrorSuppression::DoNotSuppress:
|
||||
break;
|
||||
}
|
||||
|
||||
reportError(TypesAreUnrelated{computedType, annotationType}, expr->location);
|
||||
}
|
||||
|
||||
@ -2615,25 +2357,65 @@ struct TypeChecker2
|
||||
}
|
||||
}
|
||||
|
||||
struct Reasonings
|
||||
{
|
||||
// the list of reasons
|
||||
std::vector<std::string> reasons;
|
||||
|
||||
// this should be true if _all_ of the reasons have an error suppressing type, and false otherwise.
|
||||
bool suppressed;
|
||||
|
||||
std::string toString()
|
||||
{
|
||||
// DenseHashSet ordering is entirely undefined, so we want to
|
||||
// sort the reasons here to achieve a stable error
|
||||
// stringification.
|
||||
std::sort(reasons.begin(), reasons.end());
|
||||
std::string allReasons;
|
||||
bool first = true;
|
||||
for (const std::string& reason : reasons)
|
||||
{
|
||||
if (first)
|
||||
first = false;
|
||||
else
|
||||
allReasons += "\n\t";
|
||||
|
||||
allReasons += reason;
|
||||
}
|
||||
|
||||
return allReasons;
|
||||
}
|
||||
};
|
||||
|
||||
template<typename TID>
|
||||
std::optional<std::string> explainReasonings(TID subTy, TID superTy, Location location, const SubtypingResult& r)
|
||||
Reasonings explainReasonings(TID subTy, TID superTy, Location location, const SubtypingResult& r)
|
||||
{
|
||||
if (r.reasoning.empty())
|
||||
return std::nullopt;
|
||||
return {};
|
||||
|
||||
std::vector<std::string> reasons;
|
||||
bool suppressed = true;
|
||||
for (const SubtypingReasoning& reasoning : r.reasoning)
|
||||
{
|
||||
if (reasoning.subPath.empty() && reasoning.superPath.empty())
|
||||
continue;
|
||||
|
||||
std::optional<TypeOrPack> subLeaf = traverse(subTy, reasoning.subPath, builtinTypes);
|
||||
std::optional<TypeOrPack> superLeaf = traverse(superTy, reasoning.superPath, builtinTypes);
|
||||
std::optional<TypeOrPack> optSubLeaf = traverse(subTy, reasoning.subPath, builtinTypes);
|
||||
std::optional<TypeOrPack> optSuperLeaf = traverse(superTy, reasoning.superPath, builtinTypes);
|
||||
|
||||
if (!subLeaf || !superLeaf)
|
||||
if (!optSubLeaf || !optSuperLeaf)
|
||||
ice->ice("Subtyping test returned a reasoning with an invalid path", location);
|
||||
|
||||
if (!get2<TypeId, TypeId>(*subLeaf, *superLeaf) && !get2<TypePackId, TypePackId>(*subLeaf, *superLeaf))
|
||||
const TypeOrPack& subLeaf = *optSubLeaf;
|
||||
const TypeOrPack& superLeaf = *optSuperLeaf;
|
||||
|
||||
auto subLeafTy = get<TypeId>(subLeaf);
|
||||
auto superLeafTy = get<TypeId>(superLeaf);
|
||||
|
||||
auto subLeafTp = get<TypePackId>(subLeaf);
|
||||
auto superLeafTp = get<TypePackId>(superLeaf);
|
||||
|
||||
if (!subLeafTy && !superLeafTy && !subLeafTp && !superLeafTp)
|
||||
ice->ice("Subtyping test returned a reasoning where one path ends at a type and the other ends at a pack.", location);
|
||||
|
||||
std::string relation = "a subtype of";
|
||||
@ -2644,41 +2426,61 @@ struct TypeChecker2
|
||||
|
||||
std::string reason;
|
||||
if (reasoning.subPath == reasoning.superPath)
|
||||
reason = "at " + toString(reasoning.subPath) + ", " + toString(*subLeaf) + " is not " + relation + " " + toString(*superLeaf);
|
||||
reason = "at " + toString(reasoning.subPath) + ", " + toString(subLeaf) + " is not " + relation + " " + toString(superLeaf);
|
||||
else
|
||||
reason = "type " + toString(subTy) + toString(reasoning.subPath, /* prefixDot */ true) + " (" + toString(*subLeaf) + ") is not " +
|
||||
relation + " " + toString(superTy) + toString(reasoning.superPath, /* prefixDot */ true) + " (" + toString(*superLeaf) + ")";
|
||||
reason = "type " + toString(subTy) + toString(reasoning.subPath, /* prefixDot */ true) + " (" + toString(subLeaf) + ") is not " +
|
||||
relation + " " + toString(superTy) + toString(reasoning.superPath, /* prefixDot */ true) + " (" + toString(superLeaf) + ")";
|
||||
|
||||
reasons.push_back(reason);
|
||||
|
||||
// if we haven't already proved this isn't suppressing, we have to keep checking.
|
||||
if (suppressed)
|
||||
{
|
||||
if (subLeafTy && superLeafTy)
|
||||
suppressed &= isErrorSuppressing(location, *subLeafTy) || isErrorSuppressing(location, *superLeafTy);
|
||||
else
|
||||
suppressed &= isErrorSuppressing(location, *subLeafTp) || isErrorSuppressing(location, *superLeafTp);
|
||||
}
|
||||
}
|
||||
|
||||
// DenseHashSet ordering is entirely undefined, so we want to
|
||||
// sort the reasons here to achieve a stable error
|
||||
// stringification.
|
||||
std::sort(reasons.begin(), reasons.end());
|
||||
std::string allReasons;
|
||||
bool first = true;
|
||||
for (const std::string& reason : reasons)
|
||||
{
|
||||
if (first)
|
||||
first = false;
|
||||
else
|
||||
allReasons += "\n\t";
|
||||
|
||||
allReasons += reason;
|
||||
}
|
||||
|
||||
return allReasons;
|
||||
return {std::move(reasons), suppressed};
|
||||
}
|
||||
|
||||
|
||||
void explainError(TypeId subTy, TypeId superTy, Location location, const SubtypingResult& result)
|
||||
{
|
||||
reportError(TypeMismatch{superTy, subTy, explainReasonings(subTy, superTy, location, result).value_or("")}, location);
|
||||
switch (shouldSuppressErrors(NotNull{&normalizer}, subTy).orElse(shouldSuppressErrors(NotNull{&normalizer}, superTy)))
|
||||
{
|
||||
case ErrorSuppression::Suppress:
|
||||
return;
|
||||
case ErrorSuppression::NormalizationFailed:
|
||||
reportError(NormalizationTooComplex{}, location);
|
||||
case ErrorSuppression::DoNotSuppress:
|
||||
break;
|
||||
}
|
||||
|
||||
Reasonings reasonings = explainReasonings(subTy, superTy, location, result);
|
||||
|
||||
if (!reasonings.suppressed)
|
||||
reportError(TypeMismatch{superTy, subTy, reasonings.toString()}, location);
|
||||
}
|
||||
|
||||
void explainError(TypePackId subTy, TypePackId superTy, Location location, const SubtypingResult& result)
|
||||
{
|
||||
reportError(TypePackMismatch{superTy, subTy, explainReasonings(subTy, superTy, location, result).value_or("")}, location);
|
||||
switch (shouldSuppressErrors(NotNull{&normalizer}, subTy).orElse(shouldSuppressErrors(NotNull{&normalizer}, superTy)))
|
||||
{
|
||||
case ErrorSuppression::Suppress:
|
||||
return;
|
||||
case ErrorSuppression::NormalizationFailed:
|
||||
reportError(NormalizationTooComplex{}, location);
|
||||
case ErrorSuppression::DoNotSuppress:
|
||||
break;
|
||||
}
|
||||
|
||||
Reasonings reasonings = explainReasonings(subTy, superTy, location, result);
|
||||
|
||||
if (!reasonings.suppressed)
|
||||
reportError(TypePackMismatch{superTy, subTy, reasonings.toString()}, location);
|
||||
}
|
||||
|
||||
bool testIsSubtype(TypeId subTy, TypeId superTy, Location location)
|
||||
@ -2688,7 +2490,7 @@ struct TypeChecker2
|
||||
if (r.normalizationTooComplex)
|
||||
reportError(NormalizationTooComplex{}, location);
|
||||
|
||||
if (!r.isSubtype && !r.isErrorSuppressing)
|
||||
if (!r.isSubtype)
|
||||
explainError(subTy, superTy, location, r);
|
||||
|
||||
return r.isSubtype;
|
||||
@ -2701,7 +2503,7 @@ struct TypeChecker2
|
||||
if (r.normalizationTooComplex)
|
||||
reportError(NormalizationTooComplex{}, location);
|
||||
|
||||
if (!r.isSubtype && !r.isErrorSuppressing)
|
||||
if (!r.isSubtype)
|
||||
explainError(subTy, superTy, location, r);
|
||||
|
||||
return r.isSubtype;
|
||||
|
@ -6,6 +6,10 @@
|
||||
|
||||
#include <stdexcept>
|
||||
|
||||
LUAU_FASTFLAGVARIABLE(LuauFollowEmptyTypePacks, false);
|
||||
|
||||
LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution);
|
||||
|
||||
namespace Luau
|
||||
{
|
||||
|
||||
@ -267,6 +271,8 @@ TypePackId follow(TypePackId tp, const void* context, TypePackId (*mapper)(const
|
||||
|
||||
if (const Unifiable::Bound<TypePackId>* btv = get<Unifiable::Bound<TypePackId>>(mapped))
|
||||
return btv->boundTo;
|
||||
else if (const TypePack* tp = get<TypePack>(mapped); (FFlag::DebugLuauDeferredConstraintResolution || FFlag::LuauFollowEmptyTypePacks) && tp && tp->head.empty())
|
||||
return tp->tail;
|
||||
else
|
||||
return std::nullopt;
|
||||
};
|
||||
|
@ -11,6 +11,8 @@
|
||||
#include "lstate.h"
|
||||
#include "lgc.h"
|
||||
|
||||
LUAU_DYNAMIC_FASTFLAGVARIABLE(LuauCodeGenFixBufferLenCheckA64, false)
|
||||
|
||||
namespace Luau
|
||||
{
|
||||
namespace CodeGen
|
||||
@ -1533,11 +1535,15 @@ void IrLoweringA64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next)
|
||||
}
|
||||
else
|
||||
{
|
||||
// fails if offset + size >= len; we compute it as len - offset <= size
|
||||
// fails if offset + size > len; we compute it as len - offset < size
|
||||
RegisterA64 tempx = castReg(KindA64::x, temp);
|
||||
build.sub(tempx, tempx, regOp(inst.b)); // implicit uxtw
|
||||
build.cmp(tempx, uint16_t(accessSize));
|
||||
build.b(ConditionA64::LessEqual, target); // note: this is a signed 64-bit comparison so that out of bounds offset fails
|
||||
|
||||
if (DFFlag::LuauCodeGenFixBufferLenCheckA64)
|
||||
build.b(ConditionA64::Less, target); // note: this is a signed 64-bit comparison so that out of bounds offset fails
|
||||
else
|
||||
build.b(ConditionA64::LessEqual, target); // note: this is a signed 64-bit comparison so that out of bounds offset fails
|
||||
}
|
||||
}
|
||||
else if (inst.b.kind == IrOpKind::Constant)
|
||||
|
@ -19,6 +19,7 @@ LUAU_FASTINTVARIABLE(LuauCodeGenReuseSlotLimit, 64)
|
||||
LUAU_FASTFLAGVARIABLE(DebugLuauAbortingChecks, false)
|
||||
LUAU_FASTFLAGVARIABLE(LuauReuseBufferChecks, false)
|
||||
LUAU_FASTFLAG(LuauCodegenVector)
|
||||
LUAU_DYNAMIC_FASTFLAGVARIABLE(LuauCodeGenCheckGcEffectFix, false)
|
||||
|
||||
namespace Luau
|
||||
{
|
||||
@ -1037,9 +1038,19 @@ static void constPropInInst(ConstPropState& state, IrBuilder& build, IrFunction&
|
||||
case IrCmd::CHECK_GC:
|
||||
// It is enough to perform a GC check once in a block
|
||||
if (state.checkedGc)
|
||||
{
|
||||
kill(function, inst);
|
||||
}
|
||||
else
|
||||
{
|
||||
state.checkedGc = true;
|
||||
|
||||
if (DFFlag::LuauCodeGenCheckGcEffectFix)
|
||||
{
|
||||
// GC assist might modify table data (hash part)
|
||||
state.invalidateHeapTableData();
|
||||
}
|
||||
}
|
||||
break;
|
||||
case IrCmd::BARRIER_OBJ:
|
||||
case IrCmd::BARRIER_TABLE_FORWARD:
|
||||
|
@ -184,6 +184,7 @@ target_sources(Luau.Analysis PRIVATE
|
||||
Analysis/include/Luau/ModuleResolver.h
|
||||
Analysis/include/Luau/NonStrictTypeChecker.h
|
||||
Analysis/include/Luau/Normalize.h
|
||||
Analysis/include/Luau/OverloadResolution.h
|
||||
Analysis/include/Luau/Predicate.h
|
||||
Analysis/include/Luau/Quantify.h
|
||||
Analysis/include/Luau/RecursionCounter.h
|
||||
@ -247,6 +248,7 @@ target_sources(Luau.Analysis PRIVATE
|
||||
Analysis/src/Module.cpp
|
||||
Analysis/src/NonStrictTypeChecker.cpp
|
||||
Analysis/src/Normalize.cpp
|
||||
Analysis/src/OverloadResolution.cpp
|
||||
Analysis/src/Quantify.cpp
|
||||
Analysis/src/Refinement.cpp
|
||||
Analysis/src/RequireTracer.cpp
|
||||
|
@ -49,7 +49,9 @@ void luaL_sandbox(lua_State* L)
|
||||
lua_pop(L, 2);
|
||||
}
|
||||
else
|
||||
{
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
|
||||
// set globals to readonly and activate safeenv since the env is immutable
|
||||
lua_setreadonly(L, LUA_GLOBALSINDEX, true);
|
||||
|
@ -7,7 +7,6 @@
|
||||
|
||||
#include <string.h>
|
||||
|
||||
|
||||
unsigned int luaS_hash(const char* str, size_t len)
|
||||
{
|
||||
// Note that this hashing algorithm is replicated in BytecodeBuilder.cpp, BytecodeBuilder::getStringHash
|
||||
|
@ -30,6 +30,7 @@ LUAU_FASTFLAG(LuauTaggedLuData)
|
||||
LUAU_FASTFLAG(LuauSciNumberSkipTrailDot)
|
||||
LUAU_DYNAMIC_FASTFLAG(LuauInterruptablePatternMatch)
|
||||
LUAU_FASTINT(CodegenHeuristicsInstructionLimit)
|
||||
LUAU_DYNAMIC_FASTFLAG(LuauCodeGenFixBufferLenCheckA64)
|
||||
|
||||
static lua_CompileOptions defaultOptions()
|
||||
{
|
||||
@ -281,6 +282,45 @@ static void* limitedRealloc(void* ud, void* ptr, size_t osize, size_t nsize)
|
||||
}
|
||||
}
|
||||
|
||||
void setupVectorHelpers(lua_State* L)
|
||||
{
|
||||
lua_pushcfunction(L, lua_vector, "vector");
|
||||
lua_setglobal(L, "vector");
|
||||
|
||||
#if LUA_VECTOR_SIZE == 4
|
||||
lua_pushvector(L, 0.0f, 0.0f, 0.0f, 0.0f);
|
||||
#else
|
||||
lua_pushvector(L, 0.0f, 0.0f, 0.0f);
|
||||
#endif
|
||||
luaL_newmetatable(L, "vector");
|
||||
|
||||
lua_pushstring(L, "__index");
|
||||
lua_pushcfunction(L, lua_vector_index, nullptr);
|
||||
lua_settable(L, -3);
|
||||
|
||||
lua_pushstring(L, "__namecall");
|
||||
lua_pushcfunction(L, lua_vector_namecall, nullptr);
|
||||
lua_settable(L, -3);
|
||||
|
||||
lua_setreadonly(L, -1, true);
|
||||
lua_setmetatable(L, -2);
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
|
||||
static void setupNativeHelpers(lua_State* L)
|
||||
{
|
||||
lua_pushcclosurek(
|
||||
L,
|
||||
[](lua_State* L) -> int {
|
||||
extern int luaG_isnative(lua_State * L, int level);
|
||||
|
||||
lua_pushboolean(L, luaG_isnative(L, 1));
|
||||
return 1;
|
||||
},
|
||||
"is_native", 0, nullptr);
|
||||
lua_setglobal(L, "is_native");
|
||||
}
|
||||
|
||||
static std::vector<Luau::CodeGen::FunctionBytecodeSummary> analyzeFile(const char* source, const unsigned nestingLimit)
|
||||
{
|
||||
Luau::BytecodeBuilder bcb;
|
||||
@ -490,27 +530,7 @@ TEST_CASE("Vector")
|
||||
runConformance(
|
||||
"vector.lua",
|
||||
[](lua_State* L) {
|
||||
lua_pushcfunction(L, lua_vector, "vector");
|
||||
lua_setglobal(L, "vector");
|
||||
|
||||
#if LUA_VECTOR_SIZE == 4
|
||||
lua_pushvector(L, 0.0f, 0.0f, 0.0f, 0.0f);
|
||||
#else
|
||||
lua_pushvector(L, 0.0f, 0.0f, 0.0f);
|
||||
#endif
|
||||
luaL_newmetatable(L, "vector");
|
||||
|
||||
lua_pushstring(L, "__index");
|
||||
lua_pushcfunction(L, lua_vector_index, nullptr);
|
||||
lua_settable(L, -3);
|
||||
|
||||
lua_pushstring(L, "__namecall");
|
||||
lua_pushcfunction(L, lua_vector_namecall, nullptr);
|
||||
lua_settable(L, -3);
|
||||
|
||||
lua_setreadonly(L, -1, true);
|
||||
lua_setmetatable(L, -2);
|
||||
lua_pop(L, 1);
|
||||
setupVectorHelpers(L);
|
||||
},
|
||||
nullptr, nullptr, nullptr);
|
||||
}
|
||||
@ -2019,7 +2039,15 @@ TEST_CASE("SafeEnv")
|
||||
|
||||
TEST_CASE("Native")
|
||||
{
|
||||
runConformance("native.lua");
|
||||
ScopedFastFlag luauCodeGenFixBufferLenCheckA64{DFFlag::LuauCodeGenFixBufferLenCheckA64, true};
|
||||
|
||||
// This tests requires code to run natively, otherwise all 'is_native' checks will fail
|
||||
if (!codegen || !luau_codegen_supported())
|
||||
return;
|
||||
|
||||
runConformance("native.lua", [](lua_State* L) {
|
||||
setupNativeHelpers(L);
|
||||
});
|
||||
}
|
||||
|
||||
TEST_CASE("NativeTypeAnnotations")
|
||||
@ -2028,37 +2056,10 @@ TEST_CASE("NativeTypeAnnotations")
|
||||
if (!codegen || !luau_codegen_supported())
|
||||
return;
|
||||
|
||||
runConformance(
|
||||
"native_types.lua",
|
||||
[](lua_State* L) {
|
||||
// add is_native() function
|
||||
lua_pushcclosurek(
|
||||
L,
|
||||
[](lua_State* L) -> int {
|
||||
extern int luaG_isnative(lua_State * L, int level);
|
||||
|
||||
lua_pushboolean(L, luaG_isnative(L, 1));
|
||||
return 1;
|
||||
},
|
||||
"is_native", 0, nullptr);
|
||||
lua_setglobal(L, "is_native");
|
||||
|
||||
// for vector tests
|
||||
lua_pushcfunction(L, lua_vector, "vector");
|
||||
lua_setglobal(L, "vector");
|
||||
|
||||
#if LUA_VECTOR_SIZE == 4
|
||||
lua_pushvector(L, 0.0f, 0.0f, 0.0f, 0.0f);
|
||||
#else
|
||||
lua_pushvector(L, 0.0f, 0.0f, 0.0f);
|
||||
#endif
|
||||
luaL_newmetatable(L, "vector");
|
||||
|
||||
lua_setreadonly(L, -1, true);
|
||||
lua_setmetatable(L, -2);
|
||||
lua_pop(L, 1);
|
||||
},
|
||||
nullptr, nullptr, nullptr);
|
||||
runConformance("native_types.lua", [](lua_State* L) {
|
||||
setupNativeHelpers(L);
|
||||
setupVectorHelpers(L);
|
||||
});
|
||||
}
|
||||
|
||||
TEST_CASE("HugeFunction")
|
||||
|
@ -13,7 +13,8 @@
|
||||
|
||||
using namespace Luau::CodeGen;
|
||||
|
||||
LUAU_FASTFLAG(LuauReuseBufferChecks);
|
||||
LUAU_FASTFLAG(LuauReuseBufferChecks)
|
||||
LUAU_DYNAMIC_FASTFLAG(LuauCodeGenCheckGcEffectFix)
|
||||
|
||||
class IrBuilderFixture
|
||||
{
|
||||
@ -2058,6 +2059,68 @@ bb_fallback_1:
|
||||
)");
|
||||
}
|
||||
|
||||
TEST_CASE_FIXTURE(IrBuilderFixture, "DuplicateHashSlotChecksInvalidation")
|
||||
{
|
||||
ScopedFastFlag luauCodeGenCheckGcEffectFix{DFFlag::LuauCodeGenCheckGcEffectFix, true};
|
||||
|
||||
IrOp block = build.block(IrBlockKind::Internal);
|
||||
IrOp fallback = build.block(IrBlockKind::Fallback);
|
||||
|
||||
build.beginBlock(block);
|
||||
|
||||
// This roughly corresponds to 'return t.a + t.a' with a stange GC assist in the middle
|
||||
IrOp table1 = build.inst(IrCmd::LOAD_POINTER, build.vmReg(1));
|
||||
IrOp slot1 = build.inst(IrCmd::GET_SLOT_NODE_ADDR, table1, build.constUint(3), build.vmConst(1));
|
||||
build.inst(IrCmd::CHECK_SLOT_MATCH, slot1, build.vmConst(1), fallback);
|
||||
IrOp value1 = build.inst(IrCmd::LOAD_TVALUE, slot1, build.constInt(0));
|
||||
build.inst(IrCmd::STORE_TVALUE, build.vmReg(3), value1);
|
||||
|
||||
build.inst(IrCmd::CHECK_GC);
|
||||
|
||||
IrOp slot1b = build.inst(IrCmd::GET_SLOT_NODE_ADDR, table1, build.constUint(8), build.vmConst(1));
|
||||
build.inst(IrCmd::CHECK_SLOT_MATCH, slot1b, build.vmConst(1), fallback);
|
||||
IrOp value1b = build.inst(IrCmd::LOAD_TVALUE, slot1b, build.constInt(0));
|
||||
build.inst(IrCmd::STORE_TVALUE, build.vmReg(4), value1b);
|
||||
|
||||
IrOp a = build.inst(IrCmd::LOAD_DOUBLE, build.vmReg(3));
|
||||
IrOp b = build.inst(IrCmd::LOAD_DOUBLE, build.vmReg(4));
|
||||
IrOp sum = build.inst(IrCmd::ADD_NUM, a, b);
|
||||
build.inst(IrCmd::STORE_DOUBLE, build.vmReg(2), sum);
|
||||
|
||||
build.inst(IrCmd::RETURN, build.vmReg(2), build.constUint(1));
|
||||
|
||||
build.beginBlock(fallback);
|
||||
build.inst(IrCmd::RETURN, build.vmReg(0), build.constUint(1));
|
||||
|
||||
updateUseCounts(build.function);
|
||||
constPropInBlockChains(build, true);
|
||||
|
||||
// In the future, we might even see duplicate identical TValue loads go away
|
||||
// In the future, we might even see loads of different VM regs with the same value go away
|
||||
CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"(
|
||||
bb_0:
|
||||
%0 = LOAD_POINTER R1
|
||||
%1 = GET_SLOT_NODE_ADDR %0, 3u, K1
|
||||
CHECK_SLOT_MATCH %1, K1, bb_fallback_1
|
||||
%3 = LOAD_TVALUE %1, 0i
|
||||
STORE_TVALUE R3, %3
|
||||
CHECK_GC
|
||||
%6 = GET_SLOT_NODE_ADDR %0, 8u, K1
|
||||
CHECK_SLOT_MATCH %6, K1, bb_fallback_1
|
||||
%8 = LOAD_TVALUE %6, 0i
|
||||
STORE_TVALUE R4, %8
|
||||
%10 = LOAD_DOUBLE R3
|
||||
%11 = LOAD_DOUBLE R4
|
||||
%12 = ADD_NUM %10, %11
|
||||
STORE_DOUBLE R2, %12
|
||||
RETURN R2, 1u
|
||||
|
||||
bb_fallback_1:
|
||||
RETURN R0, 1u
|
||||
|
||||
)");
|
||||
}
|
||||
|
||||
TEST_CASE_FIXTURE(IrBuilderFixture, "DuplicateArrayElemChecksSameIndex")
|
||||
{
|
||||
IrOp block = build.block(IrBlockKind::Internal);
|
||||
|
@ -86,6 +86,9 @@ declare function @checked contrived(n : Not<number>) : number
|
||||
declare function @checked onlyNums(...: number) : number
|
||||
declare function @checked mixedArgs(x: string, ...: number) : number
|
||||
declare function @checked optionalArg(x: string?) : number
|
||||
declare foo: {
|
||||
bar: @checked (number) -> number,
|
||||
}
|
||||
)BUILTIN_SRC";
|
||||
};
|
||||
|
||||
@ -427,7 +430,7 @@ lower(x) -- phi {x1, x2}
|
||||
)");
|
||||
|
||||
LUAU_REQUIRE_NO_ERRORS(result);
|
||||
}
|
||||
} //
|
||||
|
||||
TEST_CASE_FIXTURE(NonStrictTypeCheckerFixture, "phi_node_assignment_err")
|
||||
{
|
||||
@ -447,4 +450,28 @@ end
|
||||
NONSTRICT_REQUIRE_CHECKED_ERR(Position(8, 10), "lower", result);
|
||||
}
|
||||
|
||||
TEST_CASE_FIXTURE(NonStrictTypeCheckerFixture, "tblprop_is_checked")
|
||||
{
|
||||
CheckResult result = checkNonStrict(R"(
|
||||
foo.bar("hi")
|
||||
)");
|
||||
LUAU_REQUIRE_ERROR_COUNT(1, result);
|
||||
NONSTRICT_REQUIRE_CHECKED_ERR(Position(1, 8), "foo.bar", result);
|
||||
}
|
||||
|
||||
TEST_CASE_FIXTURE(NonStrictTypeCheckerFixture, "incorrect_arg_count")
|
||||
{
|
||||
CheckResult result = checkNonStrict(R"(
|
||||
foo.bar(1,2,3)
|
||||
abs(3, "hi");
|
||||
)");
|
||||
LUAU_REQUIRE_ERROR_COUNT(2, result);
|
||||
auto r1 = get<CheckedFunctionIncorrectArgs>(result.errors[0]);
|
||||
auto r2 = get<CheckedFunctionIncorrectArgs>(result.errors[1]);
|
||||
LUAU_ASSERT(r1);
|
||||
LUAU_ASSERT(r2);
|
||||
CHECK_EQ("abs", r1->functionName);
|
||||
CHECK_EQ("foo.bar", r2->functionName);
|
||||
}
|
||||
|
||||
TEST_SUITE_END();
|
||||
|
@ -1,11 +1,13 @@
|
||||
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
|
||||
|
||||
#include "Luau/TypeFwd.h"
|
||||
#include "Luau/TypePath.h"
|
||||
|
||||
#include "Luau/Normalize.h"
|
||||
#include "Luau/Subtyping.h"
|
||||
#include "Luau/Type.h"
|
||||
#include "Luau/TypePack.h"
|
||||
#include "Luau/TypeFamily.h"
|
||||
|
||||
#include "doctest.h"
|
||||
#include "Fixture.h"
|
||||
@ -67,6 +69,7 @@ struct SubtypeFixture : Fixture
|
||||
ScopePtr moduleScope{new Scope(rootScope)};
|
||||
|
||||
Subtyping subtyping = mkSubtyping(rootScope);
|
||||
BuiltinTypeFamilies builtinTypeFamilies{};
|
||||
|
||||
Subtyping mkSubtyping(const ScopePtr& scope)
|
||||
{
|
||||
@ -319,24 +322,6 @@ struct SubtypeFixture : Fixture
|
||||
CHECK_MESSAGE(!result.isSubtype, "Expected " << leftTy << " </: " << rightTy); \
|
||||
} while (0)
|
||||
|
||||
#define CHECK_IS_ERROR_SUPPRESSING(left, right) \
|
||||
do \
|
||||
{ \
|
||||
const auto& leftTy = (left); \
|
||||
const auto& rightTy = (right); \
|
||||
SubtypingResult result = isSubtype(leftTy, rightTy); \
|
||||
CHECK_MESSAGE(result.isErrorSuppressing, "Expected " << leftTy << " to error-suppress " << rightTy); \
|
||||
} while (0)
|
||||
|
||||
#define CHECK_IS_NOT_ERROR_SUPPRESSING(left, right) \
|
||||
do \
|
||||
{ \
|
||||
const auto& leftTy = (left); \
|
||||
const auto& rightTy = (right); \
|
||||
SubtypingResult result = isSubtype(leftTy, rightTy); \
|
||||
CHECK_MESSAGE(!result.isErrorSuppressing, "Expected " << leftTy << " to error-suppress " << rightTy); \
|
||||
} while (0)
|
||||
|
||||
/// Internal macro for registering a generated test case.
|
||||
///
|
||||
/// @param der the name of the derived fixture struct
|
||||
@ -404,11 +389,61 @@ TEST_SUITE_BEGIN("Subtyping");
|
||||
TEST_IS_SUBTYPE(builtinTypes->numberType, builtinTypes->anyType);
|
||||
TEST_IS_NOT_SUBTYPE(builtinTypes->numberType, builtinTypes->stringType);
|
||||
|
||||
TEST_CASE_FIXTURE(SubtypeFixture, "basic_reducible_sub_typefamily")
|
||||
{
|
||||
// add<number, number> <: number
|
||||
TypeId typeFamilyNum =
|
||||
arena.addType(TypeFamilyInstanceType{NotNull{&builtinTypeFamilies.addFamily}, {builtinTypes->numberType, builtinTypes->numberType}, {}});
|
||||
TypeId superTy = builtinTypes->numberType;
|
||||
SubtypingResult result = isSubtype(typeFamilyNum, superTy);
|
||||
CHECK(result.isSubtype);
|
||||
}
|
||||
|
||||
TEST_CASE_FIXTURE(SubtypeFixture, "basic_reducible_super_typefamily")
|
||||
{
|
||||
// number <: add<number, number> ~ number
|
||||
TypeId typeFamilyNum =
|
||||
arena.addType(TypeFamilyInstanceType{NotNull{&builtinTypeFamilies.addFamily}, {builtinTypes->numberType, builtinTypes->numberType}, {}});
|
||||
TypeId subTy = builtinTypes->numberType;
|
||||
SubtypingResult result = isSubtype(subTy, typeFamilyNum);
|
||||
CHECK(result.isSubtype);
|
||||
}
|
||||
|
||||
TEST_CASE_FIXTURE(SubtypeFixture, "basic_irreducible_sub_typefamily")
|
||||
{
|
||||
// add<string, boolean> ~ never <: number
|
||||
TypeId typeFamilyNum =
|
||||
arena.addType(TypeFamilyInstanceType{NotNull{&builtinTypeFamilies.addFamily}, {builtinTypes->stringType, builtinTypes->booleanType}, {}});
|
||||
TypeId superTy = builtinTypes->numberType;
|
||||
SubtypingResult result = isSubtype(typeFamilyNum, superTy);
|
||||
CHECK(result.isSubtype);
|
||||
}
|
||||
|
||||
TEST_CASE_FIXTURE(SubtypeFixture, "basic_irreducible_super_typefamily")
|
||||
{
|
||||
// number <\: add<string, boolean> ~ irreducible/never
|
||||
TypeId typeFamilyNum =
|
||||
arena.addType(TypeFamilyInstanceType{NotNull{&builtinTypeFamilies.addFamily}, {builtinTypes->stringType, builtinTypes->booleanType}, {}});
|
||||
TypeId subTy = builtinTypes->numberType;
|
||||
SubtypingResult result = isSubtype(subTy, typeFamilyNum);
|
||||
CHECK(!result.isSubtype);
|
||||
}
|
||||
|
||||
TEST_CASE_FIXTURE(SubtypeFixture, "basic_typefamily_with_generics")
|
||||
{
|
||||
// <T,U>(x: T, x: U) -> add<T,U> <: (number, number) -> number
|
||||
TypeId addFamily = arena.addType(TypeFamilyInstanceType{NotNull{&builtinTypeFamilies.addFamily}, {genericT, genericU}, {}});
|
||||
FunctionType ft{{genericT, genericU}, {}, arena.addTypePack({genericT, genericU}), arena.addTypePack({addFamily})};
|
||||
TypeId functionType = arena.addType(std::move(ft));
|
||||
FunctionType superFt{arena.addTypePack({builtinTypes->numberType, builtinTypes->numberType}), arena.addTypePack({builtinTypes->numberType})};
|
||||
TypeId superFunction = arena.addType(std::move(superFt));
|
||||
SubtypingResult result = isSubtype(functionType, superFunction);
|
||||
CHECK(result.isSubtype);
|
||||
}
|
||||
|
||||
TEST_CASE_FIXTURE(SubtypeFixture, "any <!: unknown")
|
||||
{
|
||||
SubtypingResult result = isSubtype(builtinTypes->anyType, builtinTypes->unknownType);
|
||||
CHECK(!result.isSubtype);
|
||||
CHECK(result.isErrorSuppressing);
|
||||
CHECK_IS_NOT_SUBTYPE(builtinTypes->anyType, builtinTypes->unknownType);
|
||||
}
|
||||
|
||||
TEST_CASE_FIXTURE(SubtypeFixture, "number? <: unknown")
|
||||
@ -785,7 +820,7 @@ TEST_CASE_FIXTURE(SubtypeFixture, "{ @metatable { x: number } } <!: { x: number
|
||||
// Negated subtypes
|
||||
TEST_IS_NOT_SUBTYPE(negate(builtinTypes->neverType), builtinTypes->stringType);
|
||||
TEST_IS_SUBTYPE(negate(builtinTypes->unknownType), builtinTypes->stringType);
|
||||
TEST_IS_NOT_SUBTYPE(negate(builtinTypes->anyType), builtinTypes->stringType);
|
||||
TEST_IS_SUBTYPE(negate(builtinTypes->anyType), builtinTypes->stringType);
|
||||
TEST_IS_SUBTYPE(negate(meet(builtinTypes->neverType, builtinTypes->unknownType)), builtinTypes->stringType);
|
||||
TEST_IS_SUBTYPE(negate(join(builtinTypes->neverType, builtinTypes->unknownType)), builtinTypes->stringType);
|
||||
|
||||
@ -1163,18 +1198,10 @@ TEST_CASE_FIXTURE(SubtypeFixture, "dont_cache_tests_involving_cycles")
|
||||
TEST_CASE_FIXTURE(SubtypeFixture, "<T>({ x: T }) -> T <: ({ method: <T>({ x: T }) -> T, x: number }) -> number")
|
||||
{
|
||||
// <T>({ x: T }) -> T
|
||||
TypeId tableToPropType = arena.addType(FunctionType{
|
||||
{genericT},
|
||||
{},
|
||||
arena.addTypePack({tbl({{"x", genericT}})}),
|
||||
arena.addTypePack({genericT})
|
||||
});
|
||||
TypeId tableToPropType = arena.addType(FunctionType{{genericT}, {}, arena.addTypePack({tbl({{"x", genericT}})}), arena.addTypePack({genericT})});
|
||||
|
||||
// ({ method: <T>({ x: T }) -> T, x: number }) -> number
|
||||
TypeId otherType = fn(
|
||||
{tbl({{"method", tableToPropType}, {"x", builtinTypes->numberType}})},
|
||||
{builtinTypes->numberType}
|
||||
);
|
||||
TypeId otherType = fn({tbl({{"method", tableToPropType}, {"x", builtinTypes->numberType}})}, {builtinTypes->numberType});
|
||||
|
||||
CHECK_IS_SUBTYPE(tableToPropType, otherType);
|
||||
}
|
||||
|
@ -221,20 +221,18 @@ n2 [label="number"];
|
||||
n1 -> n3 [label="y"];
|
||||
n3 [label="FunctionType 3"];
|
||||
n3 -> n4 [label="arg"];
|
||||
n4 [label="TypePack 4"];
|
||||
n4 -> n5 [label="tail"];
|
||||
n5 [label="VariadicTypePack 5"];
|
||||
n5 -> n6;
|
||||
n6 [label="string"];
|
||||
n3 -> n7 [label="ret"];
|
||||
n7 [label="TypePack 7"];
|
||||
n1 -> n8 [label="[index]"];
|
||||
n8 [label="string"];
|
||||
n1 -> n9 [label="[value]"];
|
||||
n9 [label="any"];
|
||||
n1 -> n10 [label="typeParam"];
|
||||
n10 [label="number"];
|
||||
n1 -> n5 [label="typePackParam"];
|
||||
n4 [label="VariadicTypePack 4"];
|
||||
n4 -> n5;
|
||||
n5 [label="string"];
|
||||
n3 -> n6 [label="ret"];
|
||||
n6 [label="TypePack 6"];
|
||||
n1 -> n7 [label="[index]"];
|
||||
n7 [label="string"];
|
||||
n1 -> n8 [label="[value]"];
|
||||
n8 [label="any"];
|
||||
n1 -> n9 [label="typeParam"];
|
||||
n9 [label="number"];
|
||||
n1 -> n4 [label="typePackParam"];
|
||||
})",
|
||||
toDot(requireType("a"), opts));
|
||||
}
|
||||
|
@ -956,11 +956,11 @@ caused by:
|
||||
Property 'd' is not compatible.
|
||||
Type 'string' could not be converted into 'number' in an invariant context)";
|
||||
//clang-format on
|
||||
//
|
||||
std::string actual = toString(result.errors[0]);
|
||||
|
||||
LUAU_REQUIRE_ERROR_COUNT(1, result);
|
||||
|
||||
std::string actual = toString(result.errors[0]);
|
||||
|
||||
CHECK(expected == actual);
|
||||
}
|
||||
|
||||
|
@ -332,11 +332,10 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "keyof_type_family_errors_if_it_has_nontable_
|
||||
local function err(idx: KeysOfMyObject): "x" | "y" | "z" return idx end
|
||||
)");
|
||||
|
||||
// FIXME: we should actually only report the type family being uninhabited error at its first use, I think?
|
||||
LUAU_REQUIRE_ERROR_COUNT(3, result);
|
||||
// FIXME(CLI-95289): we should actually only report the type family being uninhabited error at its first use, I think?
|
||||
LUAU_REQUIRE_ERROR_COUNT(2, result);
|
||||
CHECK(toString(result.errors[0]) == "Type family instance keyof<MyObject | boolean> is uninhabited");
|
||||
CHECK(toString(result.errors[1]) == "Type family instance keyof<MyObject | boolean> is uninhabited");
|
||||
CHECK(toString(result.errors[2]) == "Type family instance keyof<MyObject | boolean> is uninhabited");
|
||||
}
|
||||
|
||||
TEST_CASE_FIXTURE(BuiltinsFixture, "keyof_type_family_string_indexer")
|
||||
@ -459,11 +458,10 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "rawkeyof_type_family_errors_if_it_has_nontab
|
||||
local function err(idx: KeysOfMyObject): "x" | "y" | "z" return idx end
|
||||
)");
|
||||
|
||||
// FIXME: we should actually only report the type family being uninhabited error at its first use, I think?
|
||||
LUAU_REQUIRE_ERROR_COUNT(3, result);
|
||||
// FIXME(CLI-95289): we should actually only report the type family being uninhabited error at its first use, I think?
|
||||
LUAU_REQUIRE_ERROR_COUNT(2, result);
|
||||
CHECK(toString(result.errors[0]) == "Type family instance rawkeyof<MyObject | boolean> is uninhabited");
|
||||
CHECK(toString(result.errors[1]) == "Type family instance rawkeyof<MyObject | boolean> is uninhabited");
|
||||
CHECK(toString(result.errors[2]) == "Type family instance rawkeyof<MyObject | boolean> is uninhabited");
|
||||
}
|
||||
|
||||
TEST_CASE_FIXTURE(BuiltinsFixture, "rawkeyof_type_family_common_subset_if_union_of_differing_tables")
|
||||
@ -533,11 +531,10 @@ TEST_CASE_FIXTURE(ClassFixture, "keyof_type_family_errors_if_it_has_nonclass_par
|
||||
local function err(idx: KeysOfMyObject): "BaseMethod" | "BaseField" return idx end
|
||||
)");
|
||||
|
||||
// FIXME: we should actually only report the type family being uninhabited error at its first use, I think?
|
||||
LUAU_REQUIRE_ERROR_COUNT(3, result);
|
||||
// FIXME(CLI-95289): we should actually only report the type family being uninhabited error at its first use, I think?
|
||||
LUAU_REQUIRE_ERROR_COUNT(2, result);
|
||||
CHECK(toString(result.errors[0]) == "Type family instance keyof<BaseClass | boolean> is uninhabited");
|
||||
CHECK(toString(result.errors[1]) == "Type family instance keyof<BaseClass | boolean> is uninhabited");
|
||||
CHECK(toString(result.errors[2]) == "Type family instance keyof<BaseClass | boolean> is uninhabited");
|
||||
}
|
||||
|
||||
TEST_CASE_FIXTURE(ClassFixture, "keyof_type_family_common_subset_if_union_of_differing_classes")
|
||||
|
@ -21,6 +21,24 @@ LUAU_FASTINT(LuauTarjanChildLimit);
|
||||
|
||||
TEST_SUITE_BEGIN("TypeInferFunctions");
|
||||
|
||||
TEST_CASE_FIXTURE(Fixture, "overload_resolution")
|
||||
{
|
||||
CheckResult result = check(R"(
|
||||
type A = (number) -> string
|
||||
type B = (string) -> number
|
||||
|
||||
local function foo(f: A & B)
|
||||
return f(1), f("five")
|
||||
end
|
||||
)");
|
||||
LUAU_REQUIRE_NO_ERRORS(result);
|
||||
TypeId t = requireType("foo");
|
||||
const FunctionType* fooType = get<FunctionType>(requireType("foo"));
|
||||
REQUIRE(fooType != nullptr);
|
||||
|
||||
CHECK(toString(t) == "(((number) -> string) & ((string) -> number)) -> (string, number)");
|
||||
}
|
||||
|
||||
TEST_CASE_FIXTURE(Fixture, "tc_function")
|
||||
{
|
||||
CheckResult result = check("function five() return 5 end");
|
||||
@ -2198,4 +2216,15 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "regex_benchmark_string_format_minimization")
|
||||
LUAU_REQUIRE_NO_ERRORS(result);
|
||||
}
|
||||
|
||||
TEST_CASE_FIXTURE(BuiltinsFixture, "subgeneric_typefamily_super_monomorphic")
|
||||
{
|
||||
CheckResult result = check(R"(
|
||||
local a: (number, number) -> number = function(a, b) return a - b end
|
||||
|
||||
a = function(a, b) return a + b end
|
||||
)");
|
||||
|
||||
LUAU_REQUIRE_NO_ERRORS(result);
|
||||
}
|
||||
|
||||
TEST_SUITE_END();
|
||||
|
@ -1307,4 +1307,16 @@ TEST_CASE_FIXTURE(Fixture, "bidirectional_checking_and_generalization_play_nice"
|
||||
CHECK("string" == toString(requireType("b")));
|
||||
}
|
||||
|
||||
TEST_CASE_FIXTURE(Fixture, "missing_generic_type_parameter")
|
||||
{
|
||||
CheckResult result = check(R"(
|
||||
function f(x: T): T return x end
|
||||
)");
|
||||
|
||||
LUAU_REQUIRE_ERROR_COUNT(2, result);
|
||||
|
||||
REQUIRE(get<UnknownSymbol>(result.errors[0]));
|
||||
REQUIRE(get<UnknownSymbol>(result.errors[1]));
|
||||
}
|
||||
|
||||
TEST_SUITE_END();
|
||||
|
@ -4026,4 +4026,37 @@ TEST_CASE_FIXTURE(Fixture, "read_ond_write_only_indexers_are_unsupported")
|
||||
CHECK(Location{{2, 18}, {2, 23}} == result.errors[1].location);
|
||||
}
|
||||
|
||||
TEST_CASE_FIXTURE(Fixture, "table_subtyping_error_suppression")
|
||||
{
|
||||
CheckResult result = check(R"(
|
||||
function one(tbl: {x: any}) end
|
||||
function two(tbl: {x: string}) one(tbl) end -- ok, string <: any and any <: string
|
||||
|
||||
function three(tbl: {x: any, y: string}) end
|
||||
function four(tbl: {x: string, y: string}) three(tbl) end -- ok, string <: any, any <: string, string <: string
|
||||
function five(tbl: {x: string, y: number}) three(tbl) end -- error, string <: any, any <: string, but number </: string
|
||||
)");
|
||||
|
||||
LUAU_REQUIRE_ERROR_COUNT(1, result);
|
||||
|
||||
TypeMismatch* tm = get<TypeMismatch>(result.errors[0]);
|
||||
REQUIRE(tm);
|
||||
|
||||
|
||||
// the new solver reports specifically the inner mismatch, rather than the whole table
|
||||
// honestly not sure which of these is a better developer experience.
|
||||
if (FFlag::DebugLuauDeferredConstraintResolution)
|
||||
{
|
||||
CHECK_EQ(*tm->wantedType, *builtinTypes->stringType);
|
||||
CHECK_EQ(*tm->givenType, *builtinTypes->numberType);
|
||||
}
|
||||
else
|
||||
{
|
||||
CHECK_EQ("{| x: any, y: string |}", toString(tm->wantedType));
|
||||
CHECK_EQ("{| x: string, y: number |}", toString(tm->givenType));
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
TEST_SUITE_END();
|
||||
|
@ -422,4 +422,34 @@ TEST_CASE_FIXTURE(TypeStateFixture, "multiple_assignments_in_loops")
|
||||
CHECK("(number | string)?" == toString(requireType("x")));
|
||||
}
|
||||
|
||||
TEST_CASE_FIXTURE(TypeStateFixture, "typestates_preserve_error_suppression")
|
||||
{
|
||||
CheckResult result = check(R"(
|
||||
local a: any = 51
|
||||
a = "pickles" -- We'll have a new DefId for this iteration of `a`. Its type must also be error-suppressing
|
||||
print(a)
|
||||
)");
|
||||
|
||||
LUAU_REQUIRE_NO_ERRORS(result);
|
||||
CHECK("*error-type* | string" == toString(requireTypeAtPosition({3, 14}), {true}));
|
||||
}
|
||||
|
||||
|
||||
TEST_CASE_FIXTURE(BuiltinsFixture, "typestates_preserve_error_suppression_properties")
|
||||
{
|
||||
// early return if the flag isn't set since this is blocking gated commits
|
||||
if (!FFlag::DebugLuauDeferredConstraintResolution)
|
||||
return;
|
||||
|
||||
CheckResult result = check(R"(
|
||||
local a: {x: any} = {x = 51}
|
||||
a.x = "pickles" -- We'll have a new DefId for this iteration of `a.x`. Its type must also be error-suppressing
|
||||
print(a.x)
|
||||
)");
|
||||
|
||||
LUAU_REQUIRE_NO_ERRORS(result);
|
||||
CHECK("*error-type* | string" == toString(requireTypeAtPosition({3, 16}), {true}));
|
||||
}
|
||||
|
||||
|
||||
TEST_SUITE_END();
|
||||
|
@ -167,7 +167,7 @@ end
|
||||
assert(pcall(fuzzfail17) == false)
|
||||
|
||||
local function fuzzfail18()
|
||||
return bit32.extract(7890276,0)
|
||||
return bit32.extract(7890276,0)
|
||||
end
|
||||
|
||||
assert(pcall(fuzzfail18) == true)
|
||||
@ -317,4 +317,76 @@ local function vec3mulconst(a: vector) return a * 4 end
|
||||
assert(vec3mulnum(vector(10, 20, 40), 4) == vector(40, 80, 160))
|
||||
assert(vec3mulconst(vector(10, 20, 40), 4) == vector(40, 80, 160))
|
||||
|
||||
local function bufferbounds(zero)
|
||||
local b1 = buffer.create(1)
|
||||
local b2 = buffer.create(2)
|
||||
local b4 = buffer.create(4)
|
||||
local b8 = buffer.create(8)
|
||||
local b10 = buffer.create(10)
|
||||
|
||||
-- only one valid position and size for a 1 byte buffer
|
||||
buffer.writei8(b1, zero + 0, buffer.readi8(b1, zero + 0))
|
||||
buffer.writeu8(b1, zero + 0, buffer.readu8(b1, zero + 0))
|
||||
|
||||
-- 2 byte buffer
|
||||
buffer.writei8(b2, zero + 0, buffer.readi8(b2, zero + 0))
|
||||
buffer.writeu8(b2, zero + 0, buffer.readu8(b2, zero + 0))
|
||||
buffer.writei8(b2, zero + 1, buffer.readi8(b2, zero + 1))
|
||||
buffer.writeu8(b2, zero + 1, buffer.readu8(b2, zero + 1))
|
||||
buffer.writei16(b2, zero + 0, buffer.readi16(b2, zero + 0))
|
||||
buffer.writeu16(b2, zero + 0, buffer.readu16(b2, zero + 0))
|
||||
|
||||
-- 4 byte buffer
|
||||
buffer.writei8(b4, zero + 0, buffer.readi8(b4, zero + 0))
|
||||
buffer.writeu8(b4, zero + 0, buffer.readu8(b4, zero + 0))
|
||||
buffer.writei8(b4, zero + 3, buffer.readi8(b4, zero + 3))
|
||||
buffer.writeu8(b4, zero + 3, buffer.readu8(b4, zero + 3))
|
||||
buffer.writei16(b4, zero + 0, buffer.readi16(b4, zero + 0))
|
||||
buffer.writeu16(b4, zero + 0, buffer.readu16(b4, zero + 0))
|
||||
buffer.writei16(b4, zero + 2, buffer.readi16(b4, zero + 2))
|
||||
buffer.writeu16(b4, zero + 2, buffer.readu16(b4, zero + 2))
|
||||
buffer.writei32(b4, zero + 0, buffer.readi32(b4, zero + 0))
|
||||
buffer.writeu32(b4, zero + 0, buffer.readu32(b4, zero + 0))
|
||||
buffer.writef32(b4, zero + 0, buffer.readf32(b4, zero + 0))
|
||||
|
||||
-- 8 byte buffer
|
||||
buffer.writei8(b8, zero + 0, buffer.readi8(b8, zero + 0))
|
||||
buffer.writeu8(b8, zero + 0, buffer.readu8(b8, zero + 0))
|
||||
buffer.writei8(b8, zero + 7, buffer.readi8(b8, zero + 7))
|
||||
buffer.writeu8(b8, zero + 7, buffer.readu8(b8, zero + 7))
|
||||
buffer.writei16(b8, zero + 0, buffer.readi16(b8, zero + 0))
|
||||
buffer.writeu16(b8, zero + 0, buffer.readu16(b8, zero + 0))
|
||||
buffer.writei16(b8, zero + 6, buffer.readi16(b8, zero + 6))
|
||||
buffer.writeu16(b8, zero + 6, buffer.readu16(b8, zero + 6))
|
||||
buffer.writei32(b8, zero + 0, buffer.readi32(b8, zero + 0))
|
||||
buffer.writeu32(b8, zero + 0, buffer.readu32(b8, zero + 0))
|
||||
buffer.writef32(b8, zero + 0, buffer.readf32(b8, zero + 0))
|
||||
buffer.writei32(b8, zero + 4, buffer.readi32(b8, zero + 4))
|
||||
buffer.writeu32(b8, zero + 4, buffer.readu32(b8, zero + 4))
|
||||
buffer.writef32(b8, zero + 4, buffer.readf32(b8, zero + 4))
|
||||
buffer.writef64(b8, zero + 0, buffer.readf64(b8, zero + 0))
|
||||
|
||||
-- 'any' size buffer
|
||||
buffer.writei8(b10, zero + 0, buffer.readi8(b10, zero + 0))
|
||||
buffer.writeu8(b10, zero + 0, buffer.readu8(b10, zero + 0))
|
||||
buffer.writei8(b10, zero + 9, buffer.readi8(b10, zero + 9))
|
||||
buffer.writeu8(b10, zero + 9, buffer.readu8(b10, zero + 9))
|
||||
buffer.writei16(b10, zero + 0, buffer.readi16(b10, zero + 0))
|
||||
buffer.writeu16(b10, zero + 0, buffer.readu16(b10, zero + 0))
|
||||
buffer.writei16(b10, zero + 8, buffer.readi16(b10, zero + 8))
|
||||
buffer.writeu16(b10, zero + 8, buffer.readu16(b10, zero + 8))
|
||||
buffer.writei32(b10, zero + 0, buffer.readi32(b10, zero + 0))
|
||||
buffer.writeu32(b10, zero + 0, buffer.readu32(b10, zero + 0))
|
||||
buffer.writef32(b10, zero + 0, buffer.readf32(b10, zero + 0))
|
||||
buffer.writei32(b10, zero + 6, buffer.readi32(b10, zero + 6))
|
||||
buffer.writeu32(b10, zero + 6, buffer.readu32(b10, zero + 6))
|
||||
buffer.writef32(b10, zero + 6, buffer.readf32(b10, zero + 6))
|
||||
buffer.writef64(b10, zero + 0, buffer.readf64(b10, zero + 0))
|
||||
buffer.writef64(b10, zero + 2, buffer.readf64(b10, zero + 2))
|
||||
|
||||
assert(is_native())
|
||||
end
|
||||
|
||||
bufferbounds(0)
|
||||
|
||||
return('OK')
|
||||
|
@ -1,6 +1,5 @@
|
||||
AnnotationTests.typeof_expr
|
||||
AstQuery.last_argument_function_call_type
|
||||
AstQuery::getDocumentationSymbolAtPosition.table_overloaded_function_prop
|
||||
AutocompleteTest.anonymous_autofilled_generic_on_argument_type_pack_vararg
|
||||
AutocompleteTest.anonymous_autofilled_generic_type_pack_vararg
|
||||
AutocompleteTest.autocomplete_interpolated_string_as_singleton
|
||||
@ -110,7 +109,6 @@ GenericsTests.better_mismatch_error_messages
|
||||
GenericsTests.bound_tables_do_not_clone_original_fields
|
||||
GenericsTests.check_generic_function
|
||||
GenericsTests.check_generic_local_function
|
||||
GenericsTests.check_generic_typepack_function
|
||||
GenericsTests.check_mutual_generic_functions
|
||||
GenericsTests.check_nested_generic_function
|
||||
GenericsTests.check_recursive_generic_function
|
||||
@ -176,10 +174,9 @@ IntersectionTypes.overloadeded_functions_with_weird_typepacks_1
|
||||
IntersectionTypes.overloadeded_functions_with_weird_typepacks_2
|
||||
IntersectionTypes.overloadeded_functions_with_weird_typepacks_3
|
||||
IntersectionTypes.overloadeded_functions_with_weird_typepacks_4
|
||||
IntersectionTypes.select_correct_union_fn
|
||||
IntersectionTypes.should_still_pick_an_overload_whose_arguments_are_unions
|
||||
IntersectionTypes.table_write_sealed_indirect
|
||||
IntersectionTypes.union_saturate_overloaded_functions
|
||||
Linter.CleanCode
|
||||
Linter.DeprecatedApiFenv
|
||||
Linter.FormatStringTyped
|
||||
Linter.TableOperationsIndexer
|
||||
@ -197,6 +194,8 @@ NonstrictModeTests.table_props_are_any
|
||||
Normalize.higher_order_function_with_annotation
|
||||
Normalize.negations_of_tables
|
||||
Normalize.specific_functions_cannot_be_negated
|
||||
ParserTests.parse_nesting_based_end_detection
|
||||
ParserTests.parse_nesting_based_end_detection_single_line
|
||||
ProvisionalTests.assign_table_with_refined_property_with_a_similar_type_is_illegal
|
||||
ProvisionalTests.discriminate_from_x_not_equal_to_nil
|
||||
ProvisionalTests.do_not_ice_when_trying_to_pick_first_of_generic_type_pack
|
||||
@ -219,6 +218,7 @@ ProvisionalTests.table_unification_infinite_recursion
|
||||
ProvisionalTests.typeguard_inference_incomplete
|
||||
ProvisionalTests.while_body_are_also_refined
|
||||
RefinementTest.assert_a_to_be_truthy_then_assert_a_to_be_number
|
||||
RefinementTest.call_an_incompatible_function_after_using_typeguard
|
||||
RefinementTest.correctly_lookup_property_whose_base_was_previously_refined
|
||||
RefinementTest.dataflow_analysis_can_tell_refinements_when_its_appropriate_to_refine_into_nil_or_never
|
||||
RefinementTest.discriminate_from_isa_of_x
|
||||
@ -362,7 +362,6 @@ ToString.primitive
|
||||
ToString.tostring_unsee_ttv_if_array
|
||||
ToString.toStringDetailed2
|
||||
ToString.toStringErrorPack
|
||||
ToString.toStringNamedFunction_generic_pack
|
||||
ToString.toStringNamedFunction_map
|
||||
TryUnifyTests.members_of_failed_typepack_unification_are_unified_with_errorType
|
||||
TryUnifyTests.result_of_failed_typepack_unification_is_constrained
|
||||
@ -415,7 +414,6 @@ TypeInfer.recursive_function_that_invokes_itself_with_a_refinement_of_its_parame
|
||||
TypeInfer.statements_are_topologically_sorted
|
||||
TypeInfer.stringify_nested_unions_with_optionals
|
||||
TypeInfer.tc_after_error_recovery_no_replacement_name_in_error
|
||||
TypeInfer.tc_if_else_expressions_expected_type_3
|
||||
TypeInfer.type_infer_recursion_limit_no_ice
|
||||
TypeInfer.type_infer_recursion_limit_normalizer
|
||||
TypeInfer.unify_nearly_identical_recursive_types
|
||||
@ -446,9 +444,7 @@ TypeInferClasses.table_indexers_are_invariant
|
||||
TypeInferClasses.unions_of_intersections_of_classes
|
||||
TypeInferClasses.we_can_report_when_someone_is_trying_to_use_a_table_rather_than_a_class
|
||||
TypeInferFunctions.another_other_higher_order_function
|
||||
TypeInferFunctions.apply_of_lambda_with_inferred_and_explicit_types
|
||||
TypeInferFunctions.calling_function_with_anytypepack_doesnt_leak_free_types
|
||||
TypeInferFunctions.cannot_hoist_interior_defns_into_signature
|
||||
TypeInferFunctions.check_function_bodies
|
||||
TypeInferFunctions.complicated_return_types_require_an_explicit_annotation
|
||||
TypeInferFunctions.concrete_functions_are_not_supertypes_of_function
|
||||
@ -586,7 +582,6 @@ TypeInferUnknownNever.index_on_union_of_tables_for_properties_that_is_sorta_neve
|
||||
TypeInferUnknownNever.length_of_never
|
||||
TypeInferUnknownNever.math_operators_and_never
|
||||
TypeInferUnknownNever.type_packs_containing_never_is_itself_uninhabitable
|
||||
TypePackTests.detect_cyclic_typepacks2
|
||||
TypePackTests.fuzz_typepack_iter_follow_2
|
||||
TypePackTests.pack_tail_unification_check
|
||||
TypePackTests.type_alias_backwards_compatible
|
||||
@ -597,10 +592,12 @@ TypePackTests.unify_variadic_tails_in_arguments
|
||||
TypeSingletons.enums_using_singletons_mismatch
|
||||
TypeSingletons.error_detailed_tagged_union_mismatch_bool
|
||||
TypeSingletons.error_detailed_tagged_union_mismatch_string
|
||||
TypeSingletons.overloaded_function_call_with_singletons_mismatch
|
||||
TypeSingletons.return_type_of_f_is_not_widened
|
||||
TypeSingletons.table_properties_type_error_escapes
|
||||
TypeSingletons.widen_the_supertype_if_it_is_free_and_subtype_has_singleton
|
||||
TypeStatesTest.prototyped_recursive_functions_but_has_future_assignments
|
||||
TypeStatesTest.typestates_preserve_error_suppression_properties
|
||||
UnionTypes.error_detailed_optional
|
||||
UnionTypes.error_detailed_union_all
|
||||
UnionTypes.generic_function_with_optional_arg
|
||||
|
Loading…
Reference in New Issue
Block a user