diff --git a/Analysis/include/Luau/ConstraintGenerator.h b/Analysis/include/Luau/ConstraintGenerator.h index 2f746a74..044d4aa6 100644 --- a/Analysis/include/Luau/ConstraintGenerator.h +++ b/Analysis/include/Luau/ConstraintGenerator.h @@ -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 head, std::optional tail); + /** * Fabricates a scope that is a child of another scope. * @param node the lexical node that the scope belongs to. diff --git a/Analysis/include/Luau/ConstraintSolver.h b/Analysis/include/Luau/ConstraintSolver.h index e962f343..0f4be214 100644 --- a/Analysis/include/Luau/ConstraintSolver.h +++ b/Analysis/include/Luau/ConstraintSolver.h @@ -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 + template bool tryUnify(NotNull constraint, TID subTy, TID superTy); /** diff --git a/Analysis/include/Luau/Error.h b/Analysis/include/Luau/Error.h index 06ea9601..5a2caf65 100644 --- a/Analysis/include/Luau/Error.h +++ b/Analysis/include/Luau/Error.h @@ -380,13 +380,22 @@ struct NonStrictFunctionDefinitionError bool operator==(const NonStrictFunctionDefinitionError& rhs) const; }; -using TypeErrorData = Variant; +struct CheckedFunctionIncorrectArgs +{ + std::string functionName; + size_t expected; + size_t actual; + bool operator==(const CheckedFunctionIncorrectArgs& rhs) const; +}; + +using TypeErrorData = + Variant; struct TypeErrorSummary { diff --git a/Analysis/include/Luau/OverloadResolution.h b/Analysis/include/Luau/OverloadResolution.h new file mode 100644 index 00000000..42256727 --- /dev/null +++ b/Analysis/include/Luau/OverloadResolution.h @@ -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, NotNull arena, NotNull normalizer, NotNull scope, + NotNull reporter, NotNull limits, Location callLocation); + + NotNull builtinTypes; + NotNull arena; + NotNull normalizer; + NotNull scope; + NotNull ice; + NotNull limits; + Subtyping subtyping; + Location callLoc; + + // Resolver results + std::vector ok; + std::vector nonFunctions; + std::vector> arityMismatches; + std::vector> nonviableOverloads; + InsertionOrderedMap> resolution; + + + std::pair selectOverload(TypeId ty, TypePackId args); + void resolve(TypeId fnTy, const TypePack* args, AstExpr* selfExpr, const std::vector* argExprs); + +private: + std::optional testIsSubtype(const Location& location, TypeId subTy, TypeId superTy); + std::optional testIsSubtype(const Location& location, TypePackId subTy, TypePackId superTy); + std::pair checkOverload( + TypeId fnTy, const TypePack* args, AstExpr* fnLoc, const std::vector* argExprs, bool callMetamethodOk = true); + static bool isLiteral(AstExpr* expr); + LUAU_NOINLINE + std::pair checkOverload_( + TypeId fnTy, const FunctionType* fn, const TypePack* args, AstExpr* fnExpr, const std::vector* argExprs); + size_t indexof(Analysis analysis); + void add(Analysis analysis, TypeId ty, ErrorVec&& errors); +}; + +} // namespace Luau diff --git a/Analysis/include/Luau/Scope.h b/Analysis/include/Luau/Scope.h index 3f2b7355..5f1630d5 100644 --- a/Analysis/include/Luau/Scope.h +++ b/Analysis/include/Luau/Scope.h @@ -45,6 +45,8 @@ struct Scope TypeLevel level; + Location location; // the spanning location associated with this scope + std::unordered_map exportedTypeBindings; std::unordered_map privateTypeBindings; std::unordered_map typeAliasLocations; diff --git a/Analysis/include/Luau/Subtyping.h b/Analysis/include/Luau/Subtyping.h index d2619fe2..ddd12732 100644 --- a/Analysis/include/Luau/Subtyping.h +++ b/Analysis/include/Luau/Subtyping.h @@ -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 @@ -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 iceReporter; NotNull 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 TypeId makeAggregateType(const Container& container, TypeId orElse); + template + T handleTypeFamilyReductionResult(const TypeFamilyInstanceType* tf) + { + TypeFamilyContext context{arena, builtinTypes, scope, normalizer, iceReporter, NotNull{&limits}}; + TypeFamilyReductionResult 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); }; diff --git a/Analysis/include/Luau/TypeUtils.h b/Analysis/include/Luau/TypeUtils.h index 4d41926c..8803d924 100644 --- a/Analysis/include/Luau/TypeUtils.h +++ b/Analysis/include/Luau/TypeUtils.h @@ -56,11 +56,33 @@ std::vector reduceUnion(const std::vector& types); */ TypeId stripNil(NotNull 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 TryPair get2(Ty one, Ty two) { + static_assert(std::is_pointer_v, "argument must be a pointer type"); + const A* a = get(one); const B* b = get(two); if (a && b) diff --git a/Analysis/src/ConstraintGenerator.cpp b/Analysis/src/ConstraintGenerator.cpp index bc412c0e..b6c49092 100644 --- a/Analysis/src/ConstraintGenerator.cpp +++ b/Analysis/src/ConstraintGenerator.cpp @@ -167,6 +167,7 @@ void ConstraintGenerator::visitModuleRoot(AstStatBlock* block) ScopePtr scope = std::make_shared(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 head, std::optional 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(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(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 fcc = addConstraint(scope, call->func->location, @@ -2283,12 +2295,33 @@ std::optional ConstraintGenerator::checkLValue(const ScopePtr& scope, As { if (auto lt = getMutable(*ty)) ++lt->blockCount; + else if (auto ut = getMutable(*ty)) + { + for (TypeId optTy : ut->options) + if (auto lt = getMutable(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> ConstraintGenerator::createGenerics( diff --git a/Analysis/src/ConstraintSolver.cpp b/Analysis/src/ConstraintSolver.cpp index ec0d6c8a..922dc79d 100644 --- a/Analysis/src/ConstraintSolver.cpp +++ b/Analysis/src/ConstraintSolver.cpp @@ -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, NotNullanyType}; } + 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, NotNulllocation); 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(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(lt->domain); + } + else if (get(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(f); + } + else + asMutable(resultTy)->ty.emplace(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(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(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(lt->domain); - } - else if (get(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(f); - } - else - asMutable(resultTy)->ty.emplace(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); diff --git a/Analysis/src/Error.cpp b/Analysis/src/Error.cpp index 5ec2d52b..ba4e3a35 100644 --- a/Analysis/src/Error.cpp +++ b/Analysis/src/Error.cpp @@ -10,6 +10,7 @@ #include #include +#include #include 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) + { + } else static_assert(always_false_v, "Non-exhaustive type switch"); } diff --git a/Analysis/src/Frontend.cpp b/Analysis/src/Frontend.cpp index 6e991fe8..61df4ac5 100644 --- a/Analysis/src/Frontend.cpp +++ b/Analysis/src/Frontend.cpp @@ -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 Frontend::checkQueuedModules(std::optional Frontend::checkQueuedModules(std::optional checkedModules; diff --git a/Analysis/src/IostreamHelpers.cpp b/Analysis/src/IostreamHelpers.cpp index 85a03b48..72090848 100644 --- a/Analysis/src/IostreamHelpers.cpp +++ b/Analysis/src/IostreamHelpers.cpp @@ -207,6 +207,9 @@ static void errorToString(std::ostream& stream, const T& err) else if constexpr (std::is_same_v) stream << "NonStrictFunctionDefinitionError { functionName = '" + err.functionName + "', argument = '" + err.argument + "', argumentType = '" + toString(err.argumentType) + "' }"; + else if constexpr (std::is_same_v) + stream << "CheckedFunction { functionName = '" + err.functionName + ", expected = " + std::to_string(err.expected) + + ", actual = " + std::to_string(err.actual) + "}"; else static_assert(always_false_v, "Non-exhaustive type switch"); } diff --git a/Analysis/src/NonStrictTypeChecker.cpp b/Analysis/src/NonStrictTypeChecker.cpp index ef6343f2..2aeb8ebd 100644 --- a/Analysis/src/NonStrictTypeChecker.cpp +++ b/Analysis/src/NonStrictTypeChecker.cpp @@ -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 @@ -140,7 +141,6 @@ private: } std::unordered_map 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); } } } diff --git a/Analysis/src/OverloadResolution.cpp b/Analysis/src/OverloadResolution.cpp new file mode 100644 index 00000000..b7d01984 --- /dev/null +++ b/Analysis/src/OverloadResolution.cpp @@ -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, NotNull arena, NotNull normalizer, NotNull scope, + NotNull reporter, NotNull 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::selectOverload(TypeId ty, TypePackId argsPack) +{ + TypeId t = follow(ty); + if (auto it = get(t)) + { + for (TypeId component : it) + { + if (auto ftv = get(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* argExprs) +{ + fnTy = follow(fnTy); + + auto it = get(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 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 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::checkOverload( + TypeId fnTy, const TypePack* args, AstExpr* fnLoc, const std::vector* argExprs, bool callMetamethodOk) +{ + fnTy = follow(fnTy); + + ErrorVec discard; + if (get(fnTy) || get(fnTy) || get(fnTy)) + return {Ok, {}}; + else if (auto fn = get(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 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()) + return isLiteral(group->expr); + else if (auto assertion = expr->as()) + return isLiteral(assertion->expr); + + return expr->is() || expr->is() || expr->is() || + expr->is() || expr->is() || expr->is(); +} + +std::pair OverloadResolver::checkOverload_( + TypeId fnTy, const FunctionType* fn, const TypePack* args, AstExpr* fnExpr, const std::vector* 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(&reason.superPath.components.at(1))) + { + size_t nthArgument = pathIndexComponent->index; + argLocation = argExprs->at(nthArgument)->location; + + std::optional failedSubTy = traverseForType(fnTy, reason.subPath, builtinTypes); + std::optional 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 failedSubPack = traverseForPack(fnTy, reason.subPath, builtinTypes); + std::optional 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 diff --git a/Analysis/src/Subtyping.cpp b/Analysis/src/Subtyping.cpp index 62b574ca..0d621600 100644 --- a/Analysis/src/Subtyping.cpp +++ b/Analysis/src/Subtyping.cpp @@ -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& 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& results SubtypingResult SubtypingResult::any(const std::vector& 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(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(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(subTy)); // TODO: replace with ice. bool errorSuppressing = get(subTy); - result = {!errorSuppressing, errorSuppressing}; + result = {!errorSuppressing}; } else if (get(subTy)) result = {true}; else if (get(superTy)) - result = {false, true}; + result = {false}; else if (get(subTy)) - result = {false, true}; + result = {false}; else if (auto p = get2(subTy, superTy)) result = isCovariantWith(env, p.first->ty, p.second->ty).withBothComponent(TypePath::TypeField::Negated); else if (auto subNegation = get(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(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(subTy)) + result = isCovariantWith(env, subTypeFamilyInstance, superTy); + else if (auto superTypeFamilyInstance = get(superTy)) + result = isCovariantWith(env, subTy, superTypeFamilyInstance); else if (auto subGeneric = get(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(subFamilyInstance); + return isCovariantWith(env, reduced, superTy); +} + +SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const TypeId subTy, const TypeFamilyInstanceType* superFamilyInstance) +{ + // Reduce the typefamily instance + TypeId reduced = handleTypeFamilyReductionResult(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(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())); diff --git a/Analysis/src/TypeChecker2.cpp b/Analysis/src/TypeChecker2.cpp index fdac6daf..0badb2c5 100644 --- a/Analysis/src/TypeChecker2.cpp +++ b/Analysis/src/TypeChecker2.cpp @@ -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> stack; std::vector functionDeclStack; - DenseHashSet noTypeFamilyErrors{nullptr}; + DenseHashSet seenTypeFamilyInstances{nullptr}; Normalizer normalizer; Subtyping _subtyping; @@ -377,7 +378,7 @@ struct TypeChecker2 if (const AstStatExpr* stat = node->as()) { if (AstExprCall* call = stat->expr->as(); 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; - NotNull arena; - NotNull normalizer; - NotNull scope; - NotNull ice; - NotNull limits; - NotNull subtyping; - Location callLoc; - - std::vector ok; - std::vector nonFunctions; - std::vector> arityMismatches; - std::vector> nonviableOverloads; - InsertionOrderedMap> resolution; - - private: - std::optional 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 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 checkOverload( - TypeId fnTy, const TypePack* args, AstExpr* fnLoc, const std::vector* argExprs, bool callMetamethodOk = true) - { - fnTy = follow(fnTy); - - ErrorVec discard; - if (get(fnTy) || get(fnTy) || get(fnTy)) - return {Ok, {}}; - else if (auto fn = get(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 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()) - return isLiteral(group->expr); - else if (auto assertion = expr->as()) - return isLiteral(assertion->expr); - - return expr->is() || expr->is() || expr->is() || - expr->is() || expr->is() || expr->is(); - } - - LUAU_NOINLINE - std::pair checkOverload_( - TypeId fnTy, const FunctionType* fn, const TypePack* args, AstExpr* fnExpr, const std::vector* 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(&reason.superPath.components.at(1))) - { - size_t nthArgument = pathIndexComponent->index; - argLocation = argExprs->at(nthArgument)->location; - - std::optional failedSubTy = traverseForType(fnTy, reason.subPath, builtinTypes); - std::optional 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 failedSubPack = traverseForPack(fnTy, reason.subPath, builtinTypes); - std::optional 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* argExprs) - { - fnTy = follow(fnTy); - - auto it = get(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 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(exprType); cls && cls->indexer) testIsSubtype(indexType, cls->indexer->indexType, indexExpr->index->location); else if (get(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 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 - std::optional 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 reasons; + bool suppressed = true; for (const SubtypingReasoning& reasoning : r.reasoning) { if (reasoning.subPath.empty() && reasoning.superPath.empty()) continue; - std::optional subLeaf = traverse(subTy, reasoning.subPath, builtinTypes); - std::optional superLeaf = traverse(superTy, reasoning.superPath, builtinTypes); + std::optional optSubLeaf = traverse(subTy, reasoning.subPath, builtinTypes); + std::optional 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(*subLeaf, *superLeaf) && !get2(*subLeaf, *superLeaf)) + const TypeOrPack& subLeaf = *optSubLeaf; + const TypeOrPack& superLeaf = *optSuperLeaf; + + auto subLeafTy = get(subLeaf); + auto superLeafTy = get(superLeaf); + + auto subLeafTp = get(subLeaf); + auto superLeafTp = get(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; diff --git a/Analysis/src/TypePack.cpp b/Analysis/src/TypePack.cpp index 0db0e5a1..1c86f841 100644 --- a/Analysis/src/TypePack.cpp +++ b/Analysis/src/TypePack.cpp @@ -6,6 +6,10 @@ #include +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* btv = get>(mapped)) return btv->boundTo; + else if (const TypePack* tp = get(mapped); (FFlag::DebugLuauDeferredConstraintResolution || FFlag::LuauFollowEmptyTypePacks) && tp && tp->head.empty()) + return tp->tail; else return std::nullopt; }; diff --git a/CodeGen/src/IrLoweringA64.cpp b/CodeGen/src/IrLoweringA64.cpp index 182dd6c0..04804e67 100644 --- a/CodeGen/src/IrLoweringA64.cpp +++ b/CodeGen/src/IrLoweringA64.cpp @@ -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) diff --git a/CodeGen/src/OptimizeConstProp.cpp b/CodeGen/src/OptimizeConstProp.cpp index 539906c5..362dec22 100644 --- a/CodeGen/src/OptimizeConstProp.cpp +++ b/CodeGen/src/OptimizeConstProp.cpp @@ -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: diff --git a/Sources.cmake b/Sources.cmake index 680fdd8f..ae46bd36 100644 --- a/Sources.cmake +++ b/Sources.cmake @@ -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 diff --git a/VM/src/linit.cpp b/VM/src/linit.cpp index d2176a97..aad6513f 100644 --- a/VM/src/linit.cpp +++ b/VM/src/linit.cpp @@ -49,7 +49,9 @@ void luaL_sandbox(lua_State* L) lua_pop(L, 2); } else + { lua_pop(L, 1); + } // set globals to readonly and activate safeenv since the env is immutable lua_setreadonly(L, LUA_GLOBALSINDEX, true); diff --git a/VM/src/lstring.cpp b/VM/src/lstring.cpp index 62c38dd7..e57f6c29 100644 --- a/VM/src/lstring.cpp +++ b/VM/src/lstring.cpp @@ -7,7 +7,6 @@ #include - unsigned int luaS_hash(const char* str, size_t len) { // Note that this hashing algorithm is replicated in BytecodeBuilder.cpp, BytecodeBuilder::getStringHash diff --git a/tests/Conformance.test.cpp b/tests/Conformance.test.cpp index 298c5322..8d075b51 100644 --- a/tests/Conformance.test.cpp +++ b/tests/Conformance.test.cpp @@ -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 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") diff --git a/tests/IrBuilder.test.cpp b/tests/IrBuilder.test.cpp index beeabee0..6fab7440 100644 --- a/tests/IrBuilder.test.cpp +++ b/tests/IrBuilder.test.cpp @@ -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); diff --git a/tests/NonStrictTypeChecker.test.cpp b/tests/NonStrictTypeChecker.test.cpp index e4a4667f..12ad0dde 100644 --- a/tests/NonStrictTypeChecker.test.cpp +++ b/tests/NonStrictTypeChecker.test.cpp @@ -86,6 +86,9 @@ declare function @checked contrived(n : Not) : 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(result.errors[0]); + auto r2 = get(result.errors[1]); + LUAU_ASSERT(r1); + LUAU_ASSERT(r2); + CHECK_EQ("abs", r1->functionName); + CHECK_EQ("foo.bar", r2->functionName); +} + TEST_SUITE_END(); diff --git a/tests/Subtyping.test.cpp b/tests/Subtyping.test.cpp index 1b55ac6a..80a4c058 100644 --- a/tests/Subtyping.test.cpp +++ b/tests/Subtyping.test.cpp @@ -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 << " numberType, builtinTypes->anyType); TEST_IS_NOT_SUBTYPE(builtinTypes->numberType, builtinTypes->stringType); +TEST_CASE_FIXTURE(SubtypeFixture, "basic_reducible_sub_typefamily") +{ + // add <: 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 + 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 ~ 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 ~ 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") +{ + // (x: T, x: U) -> add <: (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 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 } } 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, "({ x: T }) -> T <: ({ method: ({ x: T }) -> T, x: number }) -> number") { // ({ 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: ({ 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); } diff --git a/tests/ToDot.test.cpp b/tests/ToDot.test.cpp index 13c07d11..1a0fe411 100644 --- a/tests/ToDot.test.cpp +++ b/tests/ToDot.test.cpp @@ -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)); } diff --git a/tests/ToString.test.cpp b/tests/ToString.test.cpp index a6979508..c2e4aadd 100644 --- a/tests/ToString.test.cpp +++ b/tests/ToString.test.cpp @@ -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); } diff --git a/tests/TypeFamily.test.cpp b/tests/TypeFamily.test.cpp index 6e8e3bd1..a9999123 100644 --- a/tests/TypeFamily.test.cpp +++ b/tests/TypeFamily.test.cpp @@ -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 is uninhabited"); CHECK(toString(result.errors[1]) == "Type family instance keyof is uninhabited"); - CHECK(toString(result.errors[2]) == "Type family instance keyof 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 is uninhabited"); CHECK(toString(result.errors[1]) == "Type family instance rawkeyof is uninhabited"); - CHECK(toString(result.errors[2]) == "Type family instance rawkeyof 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 is uninhabited"); CHECK(toString(result.errors[1]) == "Type family instance keyof is uninhabited"); - CHECK(toString(result.errors[2]) == "Type family instance keyof is uninhabited"); } TEST_CASE_FIXTURE(ClassFixture, "keyof_type_family_common_subset_if_union_of_differing_classes") diff --git a/tests/TypeInfer.functions.test.cpp b/tests/TypeInfer.functions.test.cpp index 21187ffe..9a563046 100644 --- a/tests/TypeInfer.functions.test.cpp +++ b/tests/TypeInfer.functions.test.cpp @@ -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(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(); diff --git a/tests/TypeInfer.generics.test.cpp b/tests/TypeInfer.generics.test.cpp index f35d4139..e145f30a 100644 --- a/tests/TypeInfer.generics.test.cpp +++ b/tests/TypeInfer.generics.test.cpp @@ -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(result.errors[0])); + REQUIRE(get(result.errors[1])); +} + TEST_SUITE_END(); diff --git a/tests/TypeInfer.tables.test.cpp b/tests/TypeInfer.tables.test.cpp index 24e0e44e..2cbc3088 100644 --- a/tests/TypeInfer.tables.test.cpp +++ b/tests/TypeInfer.tables.test.cpp @@ -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 (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(); diff --git a/tests/TypeInfer.typestates.test.cpp b/tests/TypeInfer.typestates.test.cpp index fb9213d5..d5229961 100644 --- a/tests/TypeInfer.typestates.test.cpp +++ b/tests/TypeInfer.typestates.test.cpp @@ -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(); diff --git a/tests/conformance/native.lua b/tests/conformance/native.lua index 22171077..9df7a3bb 100644 --- a/tests/conformance/native.lua +++ b/tests/conformance/native.lua @@ -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') diff --git a/tools/faillist.txt b/tools/faillist.txt index 5a98890e..82dae05f 100644 --- a/tools/faillist.txt +++ b/tools/faillist.txt @@ -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