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:
Alexander McCord 2024-02-02 13:32:42 -08:00 committed by GitHub
parent 974963a870
commit 67ce75e870
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
35 changed files with 1264 additions and 567 deletions

View file

@ -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.

View file

@ -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);
/**

View file

@ -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
{

View file

@ -0,0 +1,70 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#pragma once
#include "Luau/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

View file

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

View file

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

View file

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

View file

@ -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(

View file

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

View file

@ -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");
}

View file

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

View file

@ -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");
}

View file

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

View 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

View file

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

View file

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

View file

@ -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;
};

View file

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

View file

@ -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:

View file

@ -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

View file

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

View file

@ -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

View file

@ -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")

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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")

View file

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

View file

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

View file

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

View file

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

View file

@ -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')

View file

@ -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