// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details #include "Luau/TypeInfer.h" #include "Luau/Clone.h" #include "Luau/Common.h" #include "Luau/Instantiation.h" #include "Luau/ModuleResolver.h" #include "Luau/Normalize.h" #include "Luau/Parser.h" #include "Luau/Quantify.h" #include "Luau/RecursionCounter.h" #include "Luau/Scope.h" #include "Luau/Substitution.h" #include "Luau/TimeTrace.h" #include "Luau/TopoSortStatements.h" #include "Luau/ToString.h" #include "Luau/ToString.h" #include "Luau/TypePack.h" #include "Luau/TypeUtils.h" #include "Luau/TypeVar.h" #include "Luau/VisitTypeVar.h" #include #include LUAU_FASTFLAGVARIABLE(DebugLuauMagicTypes, false) LUAU_FASTINTVARIABLE(LuauTypeInferRecursionLimit, 165) LUAU_FASTINTVARIABLE(LuauTypeInferIterationLimit, 20000) LUAU_FASTINTVARIABLE(LuauTypeInferTypePackLoopLimit, 5000) LUAU_FASTINTVARIABLE(LuauCheckRecursionLimit, 300) LUAU_FASTINTVARIABLE(LuauVisitRecursionLimit, 500) LUAU_FASTFLAG(LuauKnowsTheDataModel3) LUAU_FASTFLAG(LuauAutocompleteDynamicLimits) LUAU_FASTFLAGVARIABLE(LuauLowerBoundsCalculation, false) LUAU_FASTFLAGVARIABLE(DebugLuauFreezeDuringUnification, false) LUAU_FASTFLAGVARIABLE(LuauSelfCallAutocompleteFix2, false) LUAU_FASTFLAGVARIABLE(LuauReduceUnionRecursion, false) LUAU_FASTFLAGVARIABLE(LuauOnlyMutateInstantiatedTables, false) LUAU_FASTFLAGVARIABLE(LuauUnsealedTableLiteral, false) LUAU_FASTFLAGVARIABLE(LuauReturnAnyInsteadOfICE, false) // Eventually removed as false. LUAU_FASTFLAG(LuauNormalizeFlagIsConservative) LUAU_FASTFLAGVARIABLE(LuauReturnTypeInferenceInNonstrict, false) LUAU_FASTFLAGVARIABLE(LuauRecursionLimitException, false); LUAU_FASTFLAGVARIABLE(LuauApplyTypeFunctionFix, false); LUAU_FASTFLAGVARIABLE(LuauAlwaysQuantify, false); LUAU_FASTFLAGVARIABLE(LuauReportErrorsOnIndexerKeyMismatch, false) LUAU_FASTFLAG(LuauQuantifyConstrained) LUAU_FASTFLAGVARIABLE(LuauFalsyPredicateReturnsNilInstead, false) LUAU_FASTFLAGVARIABLE(LuauNonCopyableTypeVarFields, false) namespace Luau { const char* TimeLimitError::what() const throw() { return "Typeinfer failed to complete in allotted time"; } static bool typeCouldHaveMetatable(TypeId ty) { return get(follow(ty)) || get(follow(ty)) || get(follow(ty)); } static void defaultLuauPrintLine(const std::string& s) { printf("%s\n", s.c_str()); } using PrintLineProc = decltype(&defaultLuauPrintLine); static PrintLineProc luauPrintLine = &defaultLuauPrintLine; void setPrintLine(PrintLineProc pl) { luauPrintLine = pl; } void resetPrintLine() { luauPrintLine = &defaultLuauPrintLine; } bool doesCallError(const AstExprCall* call) { const AstExprGlobal* global = call->func->as(); if (!global) return false; if (global->name == "error") return true; else if (global->name == "assert") { // assert() will error because it is missing the first argument if (call->args.size == 0) return true; if (AstExprConstantBool* expr = call->args.data[0]->as()) if (!expr->value) return true; } return false; } bool hasBreak(AstStat* node) { if (AstStatBlock* stat = node->as()) { for (size_t i = 0; i < stat->body.size; ++i) { if (hasBreak(stat->body.data[i])) return true; } return false; } else if (node->is()) { return true; } else if (AstStatIf* stat = node->as()) { if (hasBreak(stat->thenbody)) return true; if (stat->elsebody && hasBreak(stat->elsebody)) return true; return false; } else { return false; } } static bool hasReturn(const AstStat* node) { struct Searcher : AstVisitor { bool result = false; bool visit(AstStat*) override { return !result; // if we've already found a return statement, don't bother to traverse inward anymore } bool visit(AstStatReturn*) override { result = true; return false; } bool visit(AstExprFunction*) override { return false; // We don't care if the function uses a lambda that itself returns } }; Searcher searcher; const_cast(node)->visit(&searcher); return searcher.result; } // returns the last statement before the block exits, or nullptr if the block never exits const AstStat* getFallthrough(const AstStat* node) { if (const AstStatBlock* stat = node->as()) { if (stat->body.size == 0) return stat; for (size_t i = 0; i < stat->body.size - 1; ++i) { if (getFallthrough(stat->body.data[i]) == nullptr) return nullptr; } return getFallthrough(stat->body.data[stat->body.size - 1]); } else if (const AstStatIf* stat = node->as()) { if (const AstStat* thenf = getFallthrough(stat->thenbody)) return thenf; if (stat->elsebody) { if (const AstStat* elsef = getFallthrough(stat->elsebody)) return elsef; return nullptr; } else { return stat; } } else if (node->is()) { return nullptr; } else if (const AstStatExpr* stat = node->as()) { if (AstExprCall* call = stat->expr->as()) { if (doesCallError(call)) return nullptr; } return stat; } else if (const AstStatWhile* stat = node->as()) { if (AstExprConstantBool* expr = stat->condition->as()) { if (expr->value && !hasBreak(stat->body)) return nullptr; } return node; } else if (const AstStatRepeat* stat = node->as()) { if (AstExprConstantBool* expr = stat->condition->as()) { if (!expr->value && !hasBreak(stat->body)) return nullptr; } if (getFallthrough(stat->body) == nullptr) return nullptr; return node; } else { return node; } } static bool isMetamethod(const Name& name) { return name == "__index" || name == "__newindex" || name == "__call" || name == "__concat" || name == "__unm" || name == "__add" || name == "__sub" || name == "__mul" || name == "__div" || name == "__mod" || name == "__pow" || name == "__tostring" || name == "__metatable" || name == "__eq" || name == "__lt" || name == "__le" || name == "__mode"; } size_t HashBoolNamePair::operator()(const std::pair& pair) const { return std::hash()(pair.first) ^ std::hash()(pair.second); } TypeChecker::TypeChecker(ModuleResolver* resolver, InternalErrorReporter* iceHandler) : resolver(resolver) , iceHandler(iceHandler) , unifierState(iceHandler) , nilType(getSingletonTypes().nilType) , numberType(getSingletonTypes().numberType) , stringType(getSingletonTypes().stringType) , booleanType(getSingletonTypes().booleanType) , threadType(getSingletonTypes().threadType) , anyType(getSingletonTypes().anyType) , anyTypePack(getSingletonTypes().anyTypePack) , duplicateTypeAliases{{false, {}}} { globalScope = std::make_shared(globalTypes.addTypePack(TypePackVar{FreeTypePack{TypeLevel{}}})); globalScope->exportedTypeBindings["any"] = TypeFun{{}, anyType}; globalScope->exportedTypeBindings["nil"] = TypeFun{{}, nilType}; globalScope->exportedTypeBindings["number"] = TypeFun{{}, numberType}; globalScope->exportedTypeBindings["string"] = TypeFun{{}, stringType}; globalScope->exportedTypeBindings["boolean"] = TypeFun{{}, booleanType}; globalScope->exportedTypeBindings["thread"] = TypeFun{{}, threadType}; } ModulePtr TypeChecker::check(const SourceModule& module, Mode mode, std::optional environmentScope) { if (FFlag::LuauRecursionLimitException) { try { return checkWithoutRecursionCheck(module, mode, environmentScope); } catch (const RecursionLimitException&) { reportErrorCodeTooComplex(module.root->location); return std::move(currentModule); } } else { return checkWithoutRecursionCheck(module, mode, environmentScope); } } ModulePtr TypeChecker::checkWithoutRecursionCheck(const SourceModule& module, Mode mode, std::optional environmentScope) { LUAU_TIMETRACE_SCOPE("TypeChecker::check", "TypeChecker"); LUAU_TIMETRACE_ARGUMENT("module", module.name.c_str()); currentModule.reset(new Module()); currentModule->type = module.type; currentModule->allocator = module.allocator; currentModule->names = module.names; iceHandler->moduleName = module.name; if (FFlag::LuauAutocompleteDynamicLimits) { unifierState.counters.recursionLimit = FInt::LuauTypeInferRecursionLimit; unifierState.counters.iterationLimit = unifierIterationLimit ? *unifierIterationLimit : FInt::LuauTypeInferIterationLimit; } ScopePtr parentScope = environmentScope.value_or(globalScope); ScopePtr moduleScope = std::make_shared(parentScope); if (module.cyclic) moduleScope->returnType = addTypePack(TypePack{{anyType}, std::nullopt}); else moduleScope->returnType = freshTypePack(moduleScope); moduleScope->varargPack = anyTypePack; currentModule->scopes.push_back(std::make_pair(module.root->location, moduleScope)); currentModule->mode = mode; currentModuleName = module.name; if (prepareModuleScope) prepareModuleScope(module.name, currentModule->getModuleScope()); try { checkBlock(moduleScope, *module.root); } catch (const TimeLimitError&) { currentModule->timeout = true; } if (get(follow(moduleScope->returnType))) moduleScope->returnType = addTypePack(TypePack{{}, std::nullopt}); else moduleScope->returnType = anyify(moduleScope, moduleScope->returnType, Location{}); for (auto& [_, typeFun] : moduleScope->exportedTypeBindings) typeFun.type = anyify(moduleScope, typeFun.type, Location{}); prepareErrorsForDisplay(currentModule->errors); currentModule->clonePublicInterface(*iceHandler); // Clear unifier cache since it's keyed off internal types that get deallocated // This avoids fake cross-module cache hits and keeps cache size at bay when typechecking large module graphs. unifierState.cachedUnify.clear(); unifierState.cachedUnifyError.clear(); unifierState.skipCacheForType.clear(); duplicateTypeAliases.clear(); return std::move(currentModule); } void TypeChecker::check(const ScopePtr& scope, const AstStat& program) { if (auto block = program.as()) check(scope, *block); else if (auto if_ = program.as()) check(scope, *if_); else if (auto while_ = program.as()) check(scope, *while_); else if (auto repeat = program.as()) check(scope, *repeat); else if (program.is()) { } // Nothing to do else if (program.is()) { } // Nothing to do else if (auto return_ = program.as()) check(scope, *return_); else if (auto expr = program.as()) checkExprPack(scope, *expr->expr); else if (auto local = program.as()) check(scope, *local); else if (auto for_ = program.as()) check(scope, *for_); else if (auto forIn = program.as()) check(scope, *forIn); else if (auto assign = program.as()) check(scope, *assign); else if (auto assign = program.as()) check(scope, *assign); else if (program.is()) ice("Should not be calling two-argument check() on a function statement", program.location); else if (program.is()) ice("Should not be calling two-argument check() on a function statement", program.location); else if (auto typealias = program.as()) check(scope, *typealias); else if (auto global = program.as()) { TypeId globalType = resolveType(scope, *global->type); Name globalName(global->name.value); currentModule->declaredGlobals[globalName] = globalType; currentModule->getModuleScope()->bindings[global->name] = Binding{globalType, global->location}; } else if (auto global = program.as()) check(scope, *global); else if (auto global = program.as()) check(scope, *global); else if (auto errorStatement = program.as()) { const size_t oldSize = currentModule->errors.size(); for (AstStat* s : errorStatement->statements) check(scope, *s); for (AstExpr* expr : errorStatement->expressions) checkExpr(scope, *expr); // HACK: We want to run typechecking on the contents of the AstStatError, but // we don't think the type errors will be useful most of the time. currentModule->errors.resize(oldSize); } else ice("Unknown AstStat"); if (finishTime && TimeTrace::getClock() > *finishTime) throw TimeLimitError(); } // This particular overload is for do...end. If you need to not increase the scope level, use checkBlock directly. void TypeChecker::check(const ScopePtr& scope, const AstStatBlock& block) { ScopePtr child = childScope(scope, block.location); checkBlock(child, block); } void TypeChecker::checkBlock(const ScopePtr& scope, const AstStatBlock& block) { RecursionCounter _rc(&checkRecursionCount); if (FInt::LuauCheckRecursionLimit > 0 && checkRecursionCount >= FInt::LuauCheckRecursionLimit) { reportErrorCodeTooComplex(block.location); return; } if (FFlag::LuauRecursionLimitException) { try { checkBlockWithoutRecursionCheck(scope, block); } catch (const RecursionLimitException&) { reportErrorCodeTooComplex(block.location); return; } } else { checkBlockWithoutRecursionCheck(scope, block); } } void TypeChecker::checkBlockWithoutRecursionCheck(const ScopePtr& scope, const AstStatBlock& block) { int subLevel = 0; std::vector sorted(block.body.data, block.body.data + block.body.size); toposort(sorted); for (const auto& stat : sorted) { if (const auto& typealias = stat->as()) { check(scope, *typealias, subLevel, true); ++subLevel; } } auto protoIter = sorted.begin(); auto checkIter = sorted.begin(); std::unordered_map> functionDecls; auto isLocalLambda = [](AstStat* stat) -> AstStatLocal* { AstStatLocal* local = stat->as(); if (FFlag::LuauLowerBoundsCalculation && local && local->vars.size == 1 && local->values.size == 1 && local->values.data[0]->is()) return local; else return nullptr; }; auto checkBody = [&](AstStat* stat) { if (auto fun = stat->as()) { LUAU_ASSERT(functionDecls.count(stat)); auto [funTy, funScope] = functionDecls[stat]; check(scope, funTy, funScope, *fun); } else if (auto fun = stat->as()) { LUAU_ASSERT(functionDecls.count(stat)); auto [funTy, funScope] = functionDecls[stat]; check(scope, funTy, funScope, *fun); } }; while (protoIter != sorted.end()) { // protoIter walks forward // If it contains a function call (function bodies don't count), walk checkIter forward until it catches up with protoIter // For each element checkIter sees, check function bodies and unify the computed type with the prototype // If it is a function definition, add its prototype to the environment // If it is anything else, check it. // A subtlety is caused by mutually recursive functions, e.g. // ``` // function f(x) return g(x) end // function g(x) return f(x) end // ``` // These both call each other, so `f` will be ordered before `g`, so the call to `g` // is typechecked before `g` has had its body checked. For this reason, there's three // types for each function: before its body is checked, during checking its body, // and after its body is checked. // // We currently treat the before-type and the during-type as the same, // which can result in some oddness, as the before-type is usually a monotype, // and the after-type is often a polytype. For example: // // ``` // function f(x) local x: number = g(37) return x end // function g(x) return f(x) end // ``` // The before-type of g is `(X)->Y...` but during type-checking of `f` we will // unify that with `(number)->number`. The types end up being // ``` // function f(x:a):a local x: number = g(37) return x end // function g(x:number):number return f(x) end // ``` if (containsFunctionCallOrReturn(**protoIter) || (FFlag::LuauLowerBoundsCalculation && isLocalLambda(*protoIter))) { while (checkIter != protoIter) { checkBody(*checkIter); ++checkIter; } // We do check the current element, so advance checkIter beyond it. ++checkIter; check(scope, **protoIter); } else if (auto fun = (*protoIter)->as()) { std::optional expectedType; if (!fun->func->self) { if (auto name = fun->name->as()) { TypeId exprTy = checkExpr(scope, *name->expr).type; expectedType = getIndexTypeFromType(scope, exprTy, name->index.value, name->indexLocation, false); } } auto pair = checkFunctionSignature(scope, subLevel, *fun->func, fun->name->location, expectedType); auto [funTy, funScope] = pair; functionDecls[*protoIter] = pair; ++subLevel; TypeId leftType = follow(checkFunctionName(scope, *fun->name, funScope->level)); unify(funTy, leftType, fun->location); } else if (auto fun = (*protoIter)->as()) { auto pair = checkFunctionSignature(scope, subLevel, *fun->func, fun->name->location, std::nullopt); auto [funTy, funScope] = pair; functionDecls[*protoIter] = pair; ++subLevel; scope->bindings[fun->name] = {funTy, fun->name->location}; } else check(scope, **protoIter); ++protoIter; } while (checkIter != sorted.end()) { checkBody(*checkIter); ++checkIter; } checkBlockTypeAliases(scope, sorted); } LUAU_NOINLINE void TypeChecker::checkBlockTypeAliases(const ScopePtr& scope, std::vector& sorted) { for (const auto& stat : sorted) { if (const auto& typealias = stat->as()) { if (typealias->name == kParseNameError) continue; auto& bindings = typealias->exported ? scope->exportedTypeBindings : scope->privateTypeBindings; Name name = typealias->name.value; TypeId type = bindings[name].type; if (get(follow(type))) { if (FFlag::LuauNonCopyableTypeVarFields) { TypeVar* mty = asMutable(follow(type)); mty->reassign(*errorRecoveryType(anyType)); } else { *asMutable(type) = *errorRecoveryType(anyType); } reportError(TypeError{typealias->location, OccursCheckFailed{}}); } } } } static std::optional tryGetTypeGuardPredicate(const AstExprBinary& expr) { if (expr.op != AstExprBinary::Op::CompareEq && expr.op != AstExprBinary::Op::CompareNe) return std::nullopt; AstExpr* left = expr.left; AstExpr* right = expr.right; if (left->as()) std::swap(left, right); AstExprConstantString* str = right->as(); if (!str) return std::nullopt; AstExprCall* call = left->as(); if (!call) return std::nullopt; AstExprGlobal* callee = call->func->as(); if (!callee) return std::nullopt; if (callee->name != "type" && callee->name != "typeof") return std::nullopt; if (call->args.size != 1) return std::nullopt; // If ssval is not a valid constant string, we'll find out later when resolving predicate. Name ssval(str->value.data, str->value.size); bool isTypeof = callee->name == "typeof"; std::optional lvalue = tryGetLValue(*call->args.data[0]); if (!lvalue) return std::nullopt; Predicate predicate{TypeGuardPredicate{std::move(*lvalue), expr.location, ssval, isTypeof}}; if (expr.op == AstExprBinary::Op::CompareNe) return NotPredicate{{std::move(predicate)}}; return predicate; } void TypeChecker::check(const ScopePtr& scope, const AstStatIf& statement) { WithPredicate result = checkExpr(scope, *statement.condition); ScopePtr ifScope = childScope(scope, statement.thenbody->location); resolve(result.predicates, ifScope, true); check(ifScope, *statement.thenbody); if (statement.elsebody) { ScopePtr elseScope = childScope(scope, statement.elsebody->location); resolve(result.predicates, elseScope, false); check(elseScope, *statement.elsebody); } } template ErrorVec TypeChecker::canUnify_(Id subTy, Id superTy, const Location& location) { Unifier state = mkUnifier(location); return state.canUnify(subTy, superTy); } ErrorVec TypeChecker::canUnify(TypeId subTy, TypeId superTy, const Location& location) { return canUnify_(subTy, superTy, location); } ErrorVec TypeChecker::canUnify(TypePackId subTy, TypePackId superTy, const Location& location) { return canUnify_(subTy, superTy, location); } void TypeChecker::check(const ScopePtr& scope, const AstStatWhile& statement) { WithPredicate result = checkExpr(scope, *statement.condition); ScopePtr whileScope = childScope(scope, statement.body->location); resolve(result.predicates, whileScope, true); check(whileScope, *statement.body); } void TypeChecker::check(const ScopePtr& scope, const AstStatRepeat& statement) { ScopePtr repScope = childScope(scope, statement.location); checkBlock(repScope, *statement.body); checkExpr(repScope, *statement.condition); } void TypeChecker::unifyLowerBound(TypePackId subTy, TypePackId superTy, TypeLevel demotedLevel, const Location& location) { Unifier state = mkUnifier(location); state.unifyLowerBound(subTy, superTy, demotedLevel); state.log.commit(); reportErrors(state.errors); } struct Demoter : Substitution { Demoter(TypeArena* arena) : Substitution(TxnLog::empty(), arena) { } bool isDirty(TypeId ty) override { return get(ty); } bool isDirty(TypePackId tp) override { return get(tp); } TypeId clean(TypeId ty) override { auto ftv = get(ty); LUAU_ASSERT(ftv); return addType(FreeTypeVar{demotedLevel(ftv->level)}); } TypePackId clean(TypePackId tp) override { auto ftp = get(tp); LUAU_ASSERT(ftp); return addTypePack(TypePackVar{FreeTypePack{demotedLevel(ftp->level)}}); } TypeLevel demotedLevel(TypeLevel level) { return TypeLevel{level.level + 5000, level.subLevel}; } void demote(std::vector>& expectedTypes) { if (!FFlag::LuauQuantifyConstrained) return; for (std::optional& ty : expectedTypes) { if (ty) ty = substitute(*ty); } } }; void TypeChecker::check(const ScopePtr& scope, const AstStatReturn& return_) { std::vector> expectedTypes; expectedTypes.reserve(return_.list.size); TypePackIterator expectedRetCurr = begin(scope->returnType); TypePackIterator expectedRetEnd = end(scope->returnType); for (size_t i = 0; i < return_.list.size; ++i) { if (expectedRetCurr != expectedRetEnd) { expectedTypes.push_back(*expectedRetCurr); ++expectedRetCurr; } else if (auto expectedArgsTail = expectedRetCurr.tail()) { if (const VariadicTypePack* vtp = get(follow(*expectedArgsTail))) expectedTypes.push_back(vtp->ty); } } Demoter demoter{¤tModule->internalTypes}; demoter.demote(expectedTypes); TypePackId retPack = checkExprList(scope, return_.location, return_.list, false, {}, expectedTypes).type; if (FFlag::LuauReturnTypeInferenceInNonstrict ? FFlag::LuauLowerBoundsCalculation : useConstrainedIntersections()) { unifyLowerBound(retPack, scope->returnType, demoter.demotedLevel(scope->level), return_.location); return; } // HACK: Nonstrict mode gets a bit too smart and strict for us when we // start typechecking everything across module boundaries. if (isNonstrictMode() && follow(scope->returnType) == follow(currentModule->getModuleScope()->returnType)) { ErrorVec errors = tryUnify(retPack, scope->returnType, return_.location); if (!errors.empty()) currentModule->getModuleScope()->returnType = addTypePack({anyType}); return; } unify(retPack, scope->returnType, return_.location, CountMismatch::Context::Return); } template ErrorVec TypeChecker::tryUnify_(Id subTy, Id superTy, const Location& location) { Unifier state = mkUnifier(location); if (FFlag::DebugLuauFreezeDuringUnification) freeze(currentModule->internalTypes); state.tryUnify(subTy, superTy); if (FFlag::DebugLuauFreezeDuringUnification) unfreeze(currentModule->internalTypes); if (state.errors.empty()) state.log.commit(); return state.errors; } ErrorVec TypeChecker::tryUnify(TypeId subTy, TypeId superTy, const Location& location) { return tryUnify_(subTy, superTy, location); } ErrorVec TypeChecker::tryUnify(TypePackId subTy, TypePackId superTy, const Location& location) { return tryUnify_(subTy, superTy, location); } void TypeChecker::check(const ScopePtr& scope, const AstStatAssign& assign) { std::vector> expectedTypes; expectedTypes.reserve(assign.vars.size); ScopePtr moduleScope = currentModule->getModuleScope(); for (size_t i = 0; i < assign.vars.size; ++i) { AstExpr* dest = assign.vars.data[i]; if (auto a = dest->as()) { // AstExprLocal l-values will have to be checked again because their type might have been mutated during checkExprList later expectedTypes.push_back(scope->lookup(a->local)); } else if (auto a = dest->as()) { // AstExprGlobal l-values lookup is inlined here to avoid creating a global binding before checkExprList if (auto it = moduleScope->bindings.find(a->name); it != moduleScope->bindings.end()) expectedTypes.push_back(it->second.typeId); else expectedTypes.push_back(std::nullopt); } else { expectedTypes.push_back(checkLValue(scope, *dest)); } } TypePackId valuePack = checkExprList(scope, assign.location, assign.values, false, {}, expectedTypes).type; auto valueIter = begin(valuePack); auto valueEnd = end(valuePack); TypePack* growingPack = nullptr; for (size_t i = 0; i < assign.vars.size; ++i) { AstExpr* dest = assign.vars.data[i]; TypeId left = nullptr; if (dest->is() || dest->is()) left = checkLValue(scope, *dest); else left = *expectedTypes[i]; TypeId right = nullptr; Location loc = 0 == assign.values.size ? assign.location : i < assign.values.size ? assign.values.data[i]->location : assign.values.data[assign.values.size - 1]->location; if (valueIter != valueEnd) { right = follow(*valueIter); ++valueIter; } else if (growingPack) { growingPack->head.push_back(left); continue; } else if (auto tail = valueIter.tail()) { TypePackId tailPack = follow(*tail); if (get(tailPack)) right = errorRecoveryType(scope); else if (auto vtp = get(tailPack)) right = vtp->ty; else if (get(tailPack)) { *asMutable(tailPack) = TypePack{{left}}; growingPack = getMutable(tailPack); } } if (right) { if (!maybeGeneric(left) && isGeneric(right)) right = instantiate(scope, right, loc); // Setting a table entry to nil doesn't mean nil is the type of the indexer, it is just deleting the entry const TableTypeVar* destTableTypeReceivingNil = nullptr; if (auto indexExpr = dest->as(); isNil(right) && indexExpr) destTableTypeReceivingNil = getTableType(checkExpr(scope, *indexExpr->expr).type); if (!destTableTypeReceivingNil || !destTableTypeReceivingNil->indexer) { // In nonstrict mode, any assignments where the lhs is free and rhs isn't a function, we give it any typevar. if (isNonstrictMode() && get(follow(left)) && !get(follow(right))) unify(anyType, left, loc); else unify(right, left, loc); } } } } void TypeChecker::check(const ScopePtr& scope, const AstStatCompoundAssign& assign) { AstExprBinary expr(assign.location, assign.op, assign.var, assign.value); TypeId left = checkExpr(scope, *expr.left).type; TypeId right = checkExpr(scope, *expr.right).type; TypeId result = checkBinaryOperation(scope, expr, left, right); unify(result, left, assign.location); } void TypeChecker::check(const ScopePtr& scope, const AstStatLocal& local) { // Important subtlety: A local variable is not in scope while its initializer is being evaluated. // For instance, you cannot do this: // local a = function() return a end AstLocal** vars = local.vars.data; std::vector> varBindings; varBindings.reserve(local.vars.size); std::vector variableTypes; variableTypes.reserve(local.vars.size); std::vector> expectedTypes; expectedTypes.reserve(local.vars.size); std::vector instantiateGenerics; for (size_t i = 0; i < local.vars.size; ++i) { const AstType* annotation = vars[i]->annotation; const bool rhsIsTable = local.values.size > i && local.values.data[i]->as(); TypeId ty = nullptr; if (annotation) { ty = resolveType(scope, *annotation); // If the annotation type has an error, treat it as if there was no annotation if (get(follow(ty))) ty = nullptr; } if (!ty) ty = rhsIsTable ? freshType(scope) : isNonstrictMode() ? anyType : freshType(scope); varBindings.emplace_back(vars[i], Binding{ty, vars[i]->location}); variableTypes.push_back(ty); expectedTypes.push_back(ty); instantiateGenerics.push_back(annotation != nullptr && !maybeGeneric(ty)); } if (local.values.size > 0) { TypePackId variablePack = addTypePack(variableTypes, freshTypePack(scope)); TypePackId valuePack = checkExprList(scope, local.location, local.values, /* substituteFreeForNil= */ true, instantiateGenerics, expectedTypes).type; Unifier state = mkUnifier(local.location); state.ctx = CountMismatch::Result; state.tryUnify(valuePack, variablePack); reportErrors(state.errors); state.log.commit(); // In the code 'local T = {}', we wish to ascribe the name 'T' to the type of the table for error-reporting purposes. // We also want to do this for 'local T = setmetatable(...)'. if (local.vars.size == 1 && local.values.size == 1) { const AstExpr* rhs = local.values.data[0]; std::optional ty = first(valuePack); if (ty) { if (rhs->is()) { TableTypeVar* ttv = getMutable(follow(*ty)); if (ttv && !ttv->name && scope == currentModule->getModuleScope()) ttv->syntheticName = vars[0]->name.value; } else if (const AstExprCall* call = rhs->as()) { if (const AstExprGlobal* global = call->func->as(); global && global->name == "setmetatable") { MetatableTypeVar* mtv = getMutable(follow(*ty)); if (mtv) mtv->syntheticName = vars[0]->name.value; } } } } // Handle 'require' calls, we need to import exported type bindings into the variable 'namespace' and to update binding type in non-strict // mode for (size_t i = 0; i < local.values.size && i < local.vars.size; ++i) { const AstExprCall* call = local.values.data[i]->as(); if (!call) continue; if (auto maybeRequire = matchRequire(*call)) { AstExpr* require = *maybeRequire; if (auto moduleInfo = resolver->resolveModuleInfo(currentModuleName, *require)) { const Name name{local.vars.data[i]->name.value}; if (ModulePtr module = resolver->getModule(moduleInfo->name)) scope->importedTypeBindings[name] = module->getModuleScope()->exportedTypeBindings; // In non-strict mode we force the module type on the variable, in strict mode it is already unified if (isNonstrictMode()) { auto [types, tail] = flatten(valuePack); if (i < types.size()) varBindings[i].second.typeId = types[i]; } } } } } for (const auto& [local, binding] : varBindings) scope->bindings[local] = binding; } void TypeChecker::check(const ScopePtr& scope, const AstStatFor& expr) { ScopePtr loopScope = childScope(scope, expr.location); TypeId loopVarType = numberType; if (expr.var->annotation) unify(loopVarType, resolveType(scope, *expr.var->annotation), expr.location); loopScope->bindings[expr.var] = {loopVarType, expr.var->location}; if (!expr.from) ice("Bad AstStatFor has no from expr"); if (!expr.to) ice("Bad AstStatFor has no to expr"); unify(checkExpr(loopScope, *expr.from).type, loopVarType, expr.from->location); unify(checkExpr(loopScope, *expr.to).type, loopVarType, expr.to->location); if (expr.step) unify(checkExpr(loopScope, *expr.step).type, loopVarType, expr.step->location); check(loopScope, *expr.body); } void TypeChecker::check(const ScopePtr& scope, const AstStatForIn& forin) { ScopePtr loopScope = childScope(scope, forin.location); AstLocal** vars = forin.vars.data; std::vector varTypes; varTypes.reserve(forin.vars.size); for (size_t i = 0; i < forin.vars.size; ++i) { AstType* ann = vars[i]->annotation; TypeId ty = ann ? resolveType(scope, *ann) : anyIfNonstrict(freshType(loopScope)); loopScope->bindings[vars[i]] = {ty, vars[i]->location}; varTypes.push_back(ty); } AstExpr** values = forin.values.data; AstExpr* firstValue = forin.values.data[0]; // next is a function that takes Table and an optional index of type K // next(t: Table, index: K | nil) -> (K, V) // however, pairs and ipairs are quite messy, but they both share the same types // pairs returns 'next, t, nil', thus the type would be // pairs(t: Table) -> ((Table, K | nil) -> (K, V), Table, K | nil) // ipairs returns 'next, t, 0', thus ipairs will also share the same type as pairs, except K = number // // we can also define our own custom iterators by by returning a wrapped coroutine that calls coroutine.yield // and most custom iterators does not return a table state, or returns a function that takes no additional arguments, making it optional // so we up with this catch-all type constraint that works for all use cases // (free) -> ((free) -> R, Table | nil, K | nil) if (!firstValue) ice("expected at least an iterator function value, but we parsed nothing"); TypeId iterTy = nullptr; TypePackId callRetPack = nullptr; if (forin.values.size == 1 && firstValue->is()) { AstExprCall* exprCall = firstValue->as(); callRetPack = checkExprPack(scope, *exprCall).type; callRetPack = follow(callRetPack); if (get(callRetPack)) { iterTy = freshType(scope); unify(callRetPack, addTypePack({{iterTy}, freshTypePack(scope)}), forin.location); } else if (get(callRetPack) || !first(callRetPack)) { for (TypeId var : varTypes) unify(errorRecoveryType(scope), var, forin.location); return check(loopScope, *forin.body); } else { iterTy = *first(callRetPack); iterTy = instantiate(scope, iterTy, exprCall->location); } } else { iterTy = instantiate(scope, checkExpr(scope, *firstValue).type, firstValue->location); } if (std::optional iterMM = findMetatableEntry(iterTy, "__iter", firstValue->location)) { // if __iter metamethod is present, it will be called and the results are going to be called as if they are functions // TODO: this needs to typecheck all returned values by __iter as if they were for loop arguments // the structure of the function makes it difficult to do this especially since we don't have actual expressions, only types for (TypeId var : varTypes) unify(anyType, var, forin.location); return check(loopScope, *forin.body); } if (const TableTypeVar* iterTable = get(iterTy)) { // TODO: note that this doesn't cleanly handle iteration over mixed tables and tables without an indexer // this behavior is more or less consistent with what we do for pairs(), but really both are pretty wrong and need revisiting if (iterTable->indexer) { if (varTypes.size() > 0) unify(iterTable->indexer->indexType, varTypes[0], forin.location); if (varTypes.size() > 1) unify(iterTable->indexer->indexResultType, varTypes[1], forin.location); for (size_t i = 2; i < varTypes.size(); ++i) unify(nilType, varTypes[i], forin.location); } else { TypeId varTy = errorRecoveryType(loopScope); for (TypeId var : varTypes) unify(varTy, var, forin.location); reportError(firstValue->location, GenericError{"Cannot iterate over a table without indexer"}); } return check(loopScope, *forin.body); } const FunctionTypeVar* iterFunc = get(iterTy); if (!iterFunc) { TypeId varTy = get(iterTy) ? anyType : errorRecoveryType(loopScope); for (TypeId var : varTypes) unify(varTy, var, forin.location); if (!get(iterTy) && !get(iterTy) && !get(iterTy)) reportError(firstValue->location, CannotCallNonFunction{iterTy}); return check(loopScope, *forin.body); } if (forin.values.size == 1) { TypePackId argPack = nullptr; if (firstValue->is()) { // Extract the remaining return values of the call // and check them against the parameter types of the iterator function. auto [types, tail] = flatten(callRetPack); std::vector argTypes = std::vector(types.begin() + 1, types.end()); argPack = addTypePack(TypePackVar{TypePack{std::move(argTypes), tail}}); } else { // Check if iterator function accepts 0 arguments argPack = addTypePack(TypePack{}); } Unifier state = mkUnifier(firstValue->location); checkArgumentList(loopScope, state, argPack, iterFunc->argTypes, /*argLocations*/ {}); state.log.commit(); reportErrors(state.errors); } TypePackId varPack = addTypePack(TypePackVar{TypePack{varTypes, freshTypePack(scope)}}); if (forin.values.size >= 2) { AstArray arguments{forin.values.data + 1, forin.values.size - 1}; Position start = firstValue->location.begin; Position end = values[forin.values.size - 1]->location.end; AstExprCall exprCall{Location(start, end), firstValue, arguments, /* self= */ false, Location()}; TypePackId retPack = checkExprPack(scope, exprCall).type; unify(retPack, varPack, forin.location); } else unify(iterFunc->retTypes, varPack, forin.location); check(loopScope, *forin.body); } void TypeChecker::check(const ScopePtr& scope, TypeId ty, const ScopePtr& funScope, const AstStatFunction& function) { if (auto exprName = function.name->as()) { auto& globalBindings = currentModule->getModuleScope()->bindings; Symbol name = exprName->name; Name globalName = exprName->name.value; Binding oldBinding; bool previouslyDefined = isNonstrictMode() && globalBindings.count(name); if (previouslyDefined) { oldBinding = globalBindings[name]; } globalBindings[name] = {ty, exprName->location}; checkFunctionBody(funScope, ty, *function.func); // If in nonstrict mode and allowing redefinition of global function, restore the previous definition type // in case this function has a differing signature. The signature discrepancy will be caught in checkBlock. if (previouslyDefined) { if (FFlag::LuauReturnTypeInferenceInNonstrict && FFlag::LuauLowerBoundsCalculation) quantify(funScope, ty, exprName->location); globalBindings[name] = oldBinding; } else globalBindings[name] = {quantify(funScope, ty, exprName->location), exprName->location}; return; } else if (auto name = function.name->as()) { scope->bindings[name->local] = {ty, name->local->location}; checkFunctionBody(funScope, ty, *function.func); scope->bindings[name->local] = {anyIfNonstrict(quantify(funScope, ty, name->local->location)), name->local->location}; return; } else if (auto name = function.name->as()) { TypeId exprTy = checkExpr(scope, *name->expr).type; TableTypeVar* ttv = getMutableTableType(exprTy); if (!getIndexTypeFromType(scope, exprTy, name->index.value, name->indexLocation, false)) { if (ttv || isTableIntersection(exprTy)) reportError(TypeError{function.location, CannotExtendTable{exprTy, CannotExtendTable::Property, name->index.value}}); else reportError(TypeError{function.location, OnlyTablesCanHaveMethods{exprTy}}); } ty = follow(ty); if (ttv && ttv->state != TableState::Sealed) ttv->props[name->index.value] = {ty, /* deprecated */ false, {}, name->indexLocation}; if (function.func->self) { const FunctionTypeVar* funTy = get(ty); if (!funTy) ice("Methods should be functions"); std::optional arg0 = first(funTy->argTypes); if (!arg0) ice("Methods should always have at least 1 argument (self)"); } checkFunctionBody(funScope, ty, *function.func); if (ttv && ttv->state != TableState::Sealed) ttv->props[name->index.value] = {follow(quantify(funScope, ty, name->indexLocation)), /* deprecated */ false, {}, name->indexLocation}; } else { LUAU_ASSERT(function.name->is()); ty = follow(ty); checkFunctionBody(funScope, ty, *function.func); } } void TypeChecker::check(const ScopePtr& scope, TypeId ty, const ScopePtr& funScope, const AstStatLocalFunction& function) { Name name = function.name->name.value; scope->bindings[function.name] = {ty, function.location}; checkFunctionBody(funScope, ty, *function.func); scope->bindings[function.name] = {quantify(funScope, ty, function.name->location), function.name->location}; } void TypeChecker::check(const ScopePtr& scope, const AstStatTypeAlias& typealias, int subLevel, bool forwardDeclare) { // This function should be called at most twice for each type alias. // Once with forwardDeclare, and once without. Name name = typealias.name.value; // If the alias is missing a name, we can't do anything with it. Ignore it. if (name == kParseNameError) return; std::optional binding; if (auto it = scope->exportedTypeBindings.find(name); it != scope->exportedTypeBindings.end()) binding = it->second; else if (auto it = scope->privateTypeBindings.find(name); it != scope->privateTypeBindings.end()) binding = it->second; auto& bindingsMap = typealias.exported ? scope->exportedTypeBindings : scope->privateTypeBindings; if (forwardDeclare) { if (binding) { Location location = scope->typeAliasLocations[name]; reportError(TypeError{typealias.location, DuplicateTypeDefinition{name, location}}); bindingsMap[name] = TypeFun{binding->typeParams, binding->typePackParams, errorRecoveryType(anyType)}; duplicateTypeAliases.insert({typealias.exported, name}); } else { ScopePtr aliasScope = childScope(scope, typealias.location); aliasScope->level = scope->level.incr(); aliasScope->level.subLevel = subLevel; auto [generics, genericPacks] = createGenericTypes(aliasScope, scope->level, typealias, typealias.generics, typealias.genericPacks, /* useCache = */ true); TypeId ty = freshType(aliasScope); FreeTypeVar* ftv = getMutable(ty); LUAU_ASSERT(ftv); ftv->forwardedTypeAlias = true; bindingsMap[name] = {std::move(generics), std::move(genericPacks), ty}; scope->typeAliasLocations[name] = typealias.location; } } else { // If the first pass failed (this should mean a duplicate definition), the second pass isn't going to be // interesting. if (duplicateTypeAliases.find({typealias.exported, name})) return; if (!binding) ice("Not predeclared"); ScopePtr aliasScope = childScope(scope, typealias.location); aliasScope->level = scope->level.incr(); for (auto param : binding->typeParams) { auto generic = get(param.ty); LUAU_ASSERT(generic); aliasScope->privateTypeBindings[generic->name] = TypeFun{{}, param.ty}; } for (auto param : binding->typePackParams) { auto generic = get(param.tp); LUAU_ASSERT(generic); aliasScope->privateTypePackBindings[generic->name] = param.tp; } TypeId ty = resolveType(aliasScope, *typealias.type); if (auto ttv = getMutable(follow(ty))) { // If the table is already named and we want to rename the type function, we have to bind new alias to a copy // Additionally, we can't modify types that come from other modules if (ttv->name || follow(ty)->owningArena != ¤tModule->internalTypes) { bool sameTys = std::equal(ttv->instantiatedTypeParams.begin(), ttv->instantiatedTypeParams.end(), binding->typeParams.begin(), binding->typeParams.end(), [](auto&& itp, auto&& tp) { return itp == tp.ty; }); bool sameTps = std::equal(ttv->instantiatedTypePackParams.begin(), ttv->instantiatedTypePackParams.end(), binding->typePackParams.begin(), binding->typePackParams.end(), [](auto&& itpp, auto&& tpp) { return itpp == tpp.tp; }); // Copy can be skipped if this is an identical alias if (!ttv->name || ttv->name != name || !sameTys || !sameTps) { // This is a shallow clone, original recursive links to self are not updated TableTypeVar clone = TableTypeVar{ttv->props, ttv->indexer, ttv->level, ttv->state}; clone.definitionModuleName = ttv->definitionModuleName; clone.name = name; for (auto param : binding->typeParams) clone.instantiatedTypeParams.push_back(param.ty); for (auto param : binding->typePackParams) clone.instantiatedTypePackParams.push_back(param.tp); bool isNormal = ty->normal; ty = addType(std::move(clone)); if (FFlag::LuauLowerBoundsCalculation) asMutable(ty)->normal = isNormal; } } else { ttv->name = name; ttv->instantiatedTypeParams.clear(); for (auto param : binding->typeParams) ttv->instantiatedTypeParams.push_back(param.ty); ttv->instantiatedTypePackParams.clear(); for (auto param : binding->typePackParams) ttv->instantiatedTypePackParams.push_back(param.tp); } } else if (auto mtv = getMutable(follow(ty))) { // We can't modify types that come from other modules if (follow(ty)->owningArena == ¤tModule->internalTypes) mtv->syntheticName = name; } TypeId& bindingType = bindingsMap[name].type; if (unify(ty, bindingType, typealias.location)) bindingType = ty; if (FFlag::LuauLowerBoundsCalculation) { auto [t, ok] = normalize(bindingType, currentModule, *iceHandler); bindingType = t; if (!ok) reportError(typealias.location, NormalizationTooComplex{}); } } } void TypeChecker::check(const ScopePtr& scope, const AstStatDeclareClass& declaredClass) { std::optional superTy = std::nullopt; if (declaredClass.superName) { Name superName = Name(declaredClass.superName->value); std::optional lookupType = scope->lookupType(superName); if (!lookupType) { reportError(declaredClass.location, UnknownSymbol{superName, UnknownSymbol::Type}); return; } // We don't have generic classes, so this assertion _should_ never be hit. LUAU_ASSERT(lookupType->typeParams.size() == 0 && lookupType->typePackParams.size() == 0); superTy = lookupType->type; if (!get(follow(*superTy))) { reportError(declaredClass.location, GenericError{format("Cannot use non-class type '%s' as a superclass of class '%s'", superName.c_str(), declaredClass.name.value)}); return; } } Name className(declaredClass.name.value); TypeId classTy = addType(ClassTypeVar(className, {}, superTy, std::nullopt, {}, {}, currentModuleName)); ClassTypeVar* ctv = getMutable(classTy); TypeId metaTy = addType(TableTypeVar{TableState::Sealed, scope->level}); TableTypeVar* metatable = getMutable(metaTy); ctv->metatable = metaTy; scope->exportedTypeBindings[className] = TypeFun{{}, classTy}; for (const AstDeclaredClassProp& prop : declaredClass.props) { Name propName(prop.name.value); TypeId propTy = resolveType(scope, *prop.ty); bool assignToMetatable = isMetamethod(propName); // Function types always take 'self', but this isn't reflected in the // parsed annotation. Add it here. if (prop.isMethod) { if (FunctionTypeVar* ftv = getMutable(propTy)) { ftv->argNames.insert(ftv->argNames.begin(), FunctionArgument{"self", {}}); ftv->argTypes = addTypePack(TypePack{{classTy}, ftv->argTypes}); if (FFlag::LuauSelfCallAutocompleteFix2) ftv->hasSelf = true; } } if (ctv->props.count(propName) == 0) { if (assignToMetatable) metatable->props[propName] = {propTy}; else ctv->props[propName] = {propTy}; } else { TypeId currentTy = assignToMetatable ? metatable->props[propName].type : ctv->props[propName].type; // We special-case this logic to keep the intersection flat; otherwise we // would create a ton of nested intersection types. if (const IntersectionTypeVar* itv = get(currentTy)) { std::vector options = itv->parts; options.push_back(propTy); TypeId newItv = addType(IntersectionTypeVar{std::move(options)}); if (assignToMetatable) metatable->props[propName] = {newItv}; else ctv->props[propName] = {newItv}; } else if (get(currentTy)) { TypeId intersection = addType(IntersectionTypeVar{{currentTy, propTy}}); if (assignToMetatable) metatable->props[propName] = {intersection}; else ctv->props[propName] = {intersection}; } else { reportError(declaredClass.location, GenericError{format("Cannot overload non-function class member '%s'", propName.c_str())}); } } } } void TypeChecker::check(const ScopePtr& scope, const AstStatDeclareFunction& global) { ScopePtr funScope = childFunctionScope(scope, global.location); auto [generics, genericPacks] = createGenericTypes(funScope, std::nullopt, global, global.generics, global.genericPacks); std::vector genericTys; genericTys.reserve(generics.size()); std::transform(generics.begin(), generics.end(), std::back_inserter(genericTys), [](auto&& el) { return el.ty; }); std::vector genericTps; genericTps.reserve(genericPacks.size()); std::transform(genericPacks.begin(), genericPacks.end(), std::back_inserter(genericTps), [](auto&& el) { return el.tp; }); TypePackId argPack = resolveTypePack(funScope, global.params); TypePackId retPack = resolveTypePack(funScope, global.retTypes); TypeId fnType = addType(FunctionTypeVar{funScope->level, std::move(genericTys), std::move(genericTps), argPack, retPack}); FunctionTypeVar* ftv = getMutable(fnType); ftv->argNames.reserve(global.paramNames.size); for (const auto& el : global.paramNames) ftv->argNames.push_back(FunctionArgument{el.first.value, el.second}); Name fnName(global.name.value); currentModule->declaredGlobals[fnName] = fnType; currentModule->getModuleScope()->bindings[global.name] = Binding{fnType, global.location}; } WithPredicate TypeChecker::checkExpr(const ScopePtr& scope, const AstExpr& expr, std::optional expectedType, bool forceSingleton) { RecursionCounter _rc(&checkRecursionCount); if (FInt::LuauCheckRecursionLimit > 0 && checkRecursionCount >= FInt::LuauCheckRecursionLimit) { reportErrorCodeTooComplex(expr.location); return {errorRecoveryType(scope)}; } WithPredicate result; if (auto a = expr.as()) result = checkExpr(scope, *a->expr, expectedType); else if (expr.is()) result = {nilType}; else if (const AstExprConstantBool* bexpr = expr.as()) { if (forceSingleton || (expectedType && maybeSingleton(*expectedType))) result = {singletonType(bexpr->value)}; else result = {booleanType}; } else if (const AstExprConstantString* sexpr = expr.as()) { if (forceSingleton || (expectedType && maybeSingleton(*expectedType))) result = {singletonType(std::string(sexpr->value.data, sexpr->value.size))}; else result = {stringType}; } else if (expr.is()) result = {numberType}; else if (auto a = expr.as()) result = checkExpr(scope, *a); else if (auto a = expr.as()) result = checkExpr(scope, *a); else if (auto a = expr.as()) result = checkExpr(scope, *a); else if (auto a = expr.as()) result = checkExpr(scope, *a); else if (auto a = expr.as()) result = checkExpr(scope, *a); else if (auto a = expr.as()) result = checkExpr(scope, *a); else if (auto a = expr.as()) result = checkExpr(scope, *a, expectedType); else if (auto a = expr.as()) result = checkExpr(scope, *a, expectedType); else if (auto a = expr.as()) result = checkExpr(scope, *a); else if (auto a = expr.as()) result = checkExpr(scope, *a); else if (auto a = expr.as()) result = checkExpr(scope, *a); else if (auto a = expr.as()) result = checkExpr(scope, *a); else if (auto a = expr.as()) result = checkExpr(scope, *a, expectedType); else ice("Unhandled AstExpr?"); result.type = follow(result.type); if (!currentModule->astTypes.find(&expr)) currentModule->astTypes[&expr] = result.type; if (expectedType) currentModule->astExpectedTypes[&expr] = *expectedType; return result; } WithPredicate TypeChecker::checkExpr(const ScopePtr& scope, const AstExprLocal& expr) { std::optional lvalue = tryGetLValue(expr); LUAU_ASSERT(lvalue); // Guaranteed to not be nullopt - AstExprLocal is an LValue. if (std::optional ty = resolveLValue(scope, *lvalue)) return {*ty, {TruthyPredicate{std::move(*lvalue), expr.location}}}; // TODO: tempting to ice here, but this breaks very often because our toposort doesn't enforce this constraint // ice("AstExprLocal exists but no binding definition for it?", expr.location); reportError(TypeError{expr.location, UnknownSymbol{expr.local->name.value, UnknownSymbol::Binding}}); return {errorRecoveryType(scope)}; } WithPredicate TypeChecker::checkExpr(const ScopePtr& scope, const AstExprGlobal& expr) { std::optional lvalue = tryGetLValue(expr); LUAU_ASSERT(lvalue); // Guaranteed to not be nullopt - AstExprGlobal is an LValue. if (std::optional ty = resolveLValue(scope, *lvalue)) return {*ty, {TruthyPredicate{std::move(*lvalue), expr.location}}}; reportError(TypeError{expr.location, UnknownSymbol{expr.name.value, UnknownSymbol::Binding}}); return {errorRecoveryType(scope)}; } WithPredicate TypeChecker::checkExpr(const ScopePtr& scope, const AstExprVarargs& expr) { TypePackId varargPack = checkExprPack(scope, expr).type; if (get(varargPack)) { std::vector types = flatten(varargPack).first; return {!types.empty() ? types[0] : nilType}; } else if (get(varargPack)) { TypeId head = freshType(scope); TypePackId tail = freshTypePack(scope); *asMutable(varargPack) = TypePack{{head}, tail}; return {head}; } if (get(varargPack)) return {errorRecoveryType(scope)}; else if (auto vtp = get(varargPack)) return {vtp->ty}; else if (get(varargPack)) { // TODO: Better error? reportError(expr.location, GenericError{"Trying to get a type from a variadic type parameter"}); return {errorRecoveryType(scope)}; } else ice("Unknown TypePack type in checkExpr(AstExprVarargs)!"); } WithPredicate TypeChecker::checkExpr(const ScopePtr& scope, const AstExprCall& expr) { WithPredicate result = checkExprPack(scope, expr); TypePackId retPack = follow(result.type); if (auto pack = get(retPack)) { return {pack->head.empty() ? nilType : pack->head[0], std::move(result.predicates)}; } else if (const FreeTypePack* ftp = get(retPack)) { TypeLevel level = FFlag::LuauLowerBoundsCalculation ? ftp->level : scope->level; TypeId head = freshType(level); TypePackId pack = addTypePack(TypePackVar{TypePack{{head}, freshTypePack(level)}}); unify(pack, retPack, expr.location); return {head, std::move(result.predicates)}; } if (get(retPack)) return {errorRecoveryType(scope), std::move(result.predicates)}; else if (auto vtp = get(retPack)) return {vtp->ty, std::move(result.predicates)}; else if (get(retPack)) { if (FFlag::LuauReturnAnyInsteadOfICE) return {anyType, std::move(result.predicates)}; else ice("Unexpected abstract type pack!", expr.location); } else ice("Unknown TypePack type!", expr.location); } WithPredicate TypeChecker::checkExpr(const ScopePtr& scope, const AstExprIndexName& expr) { Name name = expr.index.value; // Redundant call if we find a refined lvalue, but this function must be called in order to recursively populate astTypes. TypeId lhsType = checkExpr(scope, *expr.expr).type; if (std::optional lvalue = tryGetLValue(expr)) if (std::optional ty = resolveLValue(scope, *lvalue)) return {*ty, {TruthyPredicate{std::move(*lvalue), expr.location}}}; lhsType = stripFromNilAndReport(lhsType, expr.expr->location); if (std::optional ty = getIndexTypeFromType(scope, lhsType, name, expr.location, true)) return {*ty}; return {errorRecoveryType(scope)}; } std::optional TypeChecker::findTablePropertyRespectingMeta(TypeId lhsType, Name name, const Location& location) { ErrorVec errors; auto result = Luau::findTablePropertyRespectingMeta(errors, lhsType, name, location); reportErrors(errors); return result; } std::optional TypeChecker::findMetatableEntry(TypeId type, std::string entry, const Location& location) { ErrorVec errors; auto result = Luau::findMetatableEntry(errors, type, entry, location); reportErrors(errors); return result; } std::optional TypeChecker::getIndexTypeFromType( const ScopePtr& scope, TypeId type, const std::string& name, const Location& location, bool addErrors) { type = follow(type); if (get(type) || get(type)) return type; tablify(type); if (isString(type)) { std::optional mtIndex = findMetatableEntry(stringType, "__index", location); LUAU_ASSERT(mtIndex); type = *mtIndex; } if (TableTypeVar* tableType = getMutableTableType(type)) { if (auto it = tableType->props.find(name); it != tableType->props.end()) return it->second.type; else if (auto indexer = tableType->indexer) { // TODO: Property lookup should work with string singletons or unions thereof as the indexer key type. ErrorVec errors = tryUnify(stringType, indexer->indexType, location); if (FFlag::LuauReportErrorsOnIndexerKeyMismatch) { if (errors.empty()) return indexer->indexResultType; if (addErrors) reportError(location, UnknownProperty{type, name}); return std::nullopt; } else return indexer->indexResultType; } else if (tableType->state == TableState::Free) { TypeId result = freshType(tableType->level); tableType->props[name] = {result}; return result; } if (auto found = findTablePropertyRespectingMeta(type, name, location)) return *found; } else if (const ClassTypeVar* cls = get(type)) { const Property* prop = lookupClassProp(cls, name); if (prop) return prop->type; } else if (const UnionTypeVar* utv = get(type)) { std::vector goodOptions; std::vector badOptions; for (TypeId t : utv) { RecursionLimiter _rl(&recursionCount, FInt::LuauTypeInferRecursionLimit, "getIndexTypeForType unions"); // Not needed when we normalize types. if (get(follow(t))) return t; if (std::optional ty = getIndexTypeFromType(scope, t, name, location, false)) goodOptions.push_back(*ty); else badOptions.push_back(t); } if (!badOptions.empty()) { if (addErrors) { if (goodOptions.empty()) reportError(location, UnknownProperty{type, name}); else reportError(location, MissingUnionProperty{type, badOptions, name}); } return std::nullopt; } if (FFlag::LuauLowerBoundsCalculation) { auto [t, ok] = normalize(addType(UnionTypeVar{std::move(goodOptions)}), currentModule, *iceHandler); // FIXME Inefficient. We craft a UnionTypeVar and immediately throw it away. if (!ok) reportError(location, NormalizationTooComplex{}); return t; } else { std::vector result = reduceUnion(goodOptions); if (result.size() == 1) return result[0]; return addType(UnionTypeVar{std::move(result)}); } } else if (const IntersectionTypeVar* itv = get(type)) { std::vector parts; for (TypeId t : itv->parts) { RecursionLimiter _rl(&recursionCount, FInt::LuauTypeInferRecursionLimit, "getIndexTypeFromType intersections"); if (std::optional ty = getIndexTypeFromType(scope, t, name, location, false)) parts.push_back(*ty); } // If no parts of the intersection had the property we looked up for, it never existed at all. if (parts.empty()) { if (addErrors) reportError(location, UnknownProperty{type, name}); return std::nullopt; } if (parts.size() == 1) return parts[0]; return addType(IntersectionTypeVar{std::move(parts)}); // Not at all correct. } if (addErrors) reportError(location, UnknownProperty{type, name}); return std::nullopt; } std::vector TypeChecker::reduceUnion(const std::vector& types) { std::vector result; for (TypeId t : types) { t = follow(t); if (get(t) || get(t)) return {t}; if (const UnionTypeVar* utv = get(t)) { if (FFlag::LuauReduceUnionRecursion) { for (TypeId ty : utv) { if (FFlag::LuauNormalizeFlagIsConservative) ty = follow(ty); if (get(ty) || get(ty)) return {ty}; if (result.end() == std::find(result.begin(), result.end(), ty)) result.push_back(ty); } } else { std::vector r = reduceUnion(utv->options); for (TypeId ty : r) { ty = follow(ty); if (get(ty) || get(ty)) return {ty}; if (std::find(result.begin(), result.end(), ty) == result.end()) result.push_back(ty); } } } else if (std::find(result.begin(), result.end(), t) == result.end()) result.push_back(t); } return result; } std::optional TypeChecker::tryStripUnionFromNil(TypeId ty) { if (const UnionTypeVar* utv = get(ty)) { if (!std::any_of(begin(utv), end(utv), isNil)) return ty; std::vector result; for (TypeId option : utv) { if (!isNil(option)) result.push_back(option); } if (result.empty()) return std::nullopt; return result.size() == 1 ? result[0] : addType(UnionTypeVar{std::move(result)}); } return std::nullopt; } TypeId TypeChecker::stripFromNilAndReport(TypeId ty, const Location& location) { ty = follow(ty); if (auto utv = get(ty)) { if (!std::any_of(begin(utv), end(utv), isNil)) return ty; } if (std::optional strippedUnion = tryStripUnionFromNil(ty)) { reportError(location, OptionalValueAccess{ty}); return follow(*strippedUnion); } return ty; } WithPredicate TypeChecker::checkExpr(const ScopePtr& scope, const AstExprIndexExpr& expr) { TypeId ty = checkLValue(scope, expr); if (std::optional lvalue = tryGetLValue(expr)) if (std::optional refiTy = resolveLValue(scope, *lvalue)) return {*refiTy, {TruthyPredicate{std::move(*lvalue), expr.location}}}; return {ty}; } WithPredicate TypeChecker::checkExpr(const ScopePtr& scope, const AstExprFunction& expr, std::optional expectedType) { auto [funTy, funScope] = checkFunctionSignature(scope, 0, expr, std::nullopt, expectedType); checkFunctionBody(funScope, funTy, expr); return {quantify(funScope, funTy, expr.location)}; } TypeId TypeChecker::checkExprTable( const ScopePtr& scope, const AstExprTable& expr, const std::vector>& fieldTypes, std::optional expectedType) { TableTypeVar::Props props; std::optional indexer; const TableTypeVar* expectedTable = nullptr; if (expectedType) { if (auto ttv = get(follow(*expectedType))) { if (ttv->state == TableState::Sealed) expectedTable = ttv; } } for (size_t i = 0; i < expr.items.size; ++i) { const AstExprTable::Item& item = expr.items.data[i]; AstExpr* k = item.key; AstExpr* value = item.value; auto [keyType, valueType] = fieldTypes[i]; if (item.kind == AstExprTable::Item::List) { if (expectedTable && !indexer) indexer = expectedTable->indexer; if (indexer) { unify(numberType, indexer->indexType, value->location); unify(valueType, indexer->indexResultType, value->location); } else indexer = TableIndexer{numberType, anyIfNonstrict(valueType)}; } else if (item.kind == AstExprTable::Item::Record || item.kind == AstExprTable::Item::General) { if (auto key = k->as()) { TypeId exprType = follow(valueType); if (isNonstrictMode() && !getTableType(exprType) && !get(exprType)) exprType = anyType; if (expectedTable) { auto it = expectedTable->props.find(key->value.data); if (it != expectedTable->props.end()) { Property expectedProp = it->second; ErrorVec errors = tryUnify(exprType, expectedProp.type, k->location); if (errors.empty()) exprType = expectedProp.type; } else if (expectedTable->indexer && maybeString(expectedTable->indexer->indexType)) { ErrorVec errors = tryUnify(exprType, expectedTable->indexer->indexResultType, k->location); if (errors.empty()) exprType = expectedTable->indexer->indexResultType; } } props[key->value.data] = {exprType, /* deprecated */ false, {}, k->location}; } else { if (expectedTable && !indexer) indexer = expectedTable->indexer; if (indexer) { unify(keyType, indexer->indexType, k->location); unify(valueType, indexer->indexResultType, value->location); } else if (isNonstrictMode()) { indexer = TableIndexer{anyType, anyType}; } else { indexer = TableIndexer{keyType, valueType}; } } } } TableState state = (expr.items.size == 0 || isNonstrictMode() || FFlag::LuauUnsealedTableLiteral) ? TableState::Unsealed : TableState::Sealed; TableTypeVar table = TableTypeVar{std::move(props), indexer, scope->level, state}; table.definitionModuleName = currentModuleName; return addType(table); } WithPredicate TypeChecker::checkExpr(const ScopePtr& scope, const AstExprTable& expr, std::optional expectedType) { RecursionCounter _rc(&checkRecursionCount); if (FInt::LuauCheckRecursionLimit > 0 && checkRecursionCount >= FInt::LuauCheckRecursionLimit) { reportErrorCodeTooComplex(expr.location); return {errorRecoveryType(scope)}; } std::vector> fieldTypes(expr.items.size); const TableTypeVar* expectedTable = nullptr; const UnionTypeVar* expectedUnion = nullptr; std::optional expectedIndexType; std::optional expectedIndexResultType; if (expectedType) { if (auto ttv = get(follow(*expectedType))) { if (ttv->state == TableState::Sealed) { expectedTable = ttv; if (ttv->indexer) { expectedIndexType = ttv->indexer->indexType; expectedIndexResultType = ttv->indexer->indexResultType; } } } else if (const UnionTypeVar* utv = get(follow(*expectedType))) expectedUnion = utv; } for (size_t i = 0; i < expr.items.size; ++i) { AstExprTable::Item& item = expr.items.data[i]; std::optional expectedResultType; bool isIndexedItem = false; if (item.kind == AstExprTable::Item::List) { expectedResultType = expectedIndexResultType; isIndexedItem = true; } else if (item.kind == AstExprTable::Item::Record || item.kind == AstExprTable::Item::General) { if (auto key = item.key->as()) { if (expectedTable) { if (auto prop = expectedTable->props.find(key->value.data); prop != expectedTable->props.end()) expectedResultType = prop->second.type; else if (expectedIndexType && maybeString(*expectedIndexType)) expectedResultType = expectedIndexResultType; } else if (expectedUnion) { std::vector expectedResultTypes; for (TypeId expectedOption : expectedUnion) if (const TableTypeVar* ttv = get(follow(expectedOption))) if (auto prop = ttv->props.find(key->value.data); prop != ttv->props.end()) expectedResultTypes.push_back(prop->second.type); if (expectedResultTypes.size() == 1) expectedResultType = expectedResultTypes[0]; else if (expectedResultTypes.size() > 1) expectedResultType = addType(UnionTypeVar{expectedResultTypes}); } } else { expectedResultType = expectedIndexResultType; isIndexedItem = true; } } fieldTypes[i].first = item.key ? checkExpr(scope, *item.key, expectedIndexType).type : nullptr; fieldTypes[i].second = checkExpr(scope, *item.value, expectedResultType).type; // Indexer keys after the first are unified with the first one // If we don't have an expected indexer type yet, take this first item type if (isIndexedItem && !expectedIndexResultType) expectedIndexResultType = fieldTypes[i].second; } return {checkExprTable(scope, expr, fieldTypes, expectedType)}; } WithPredicate TypeChecker::checkExpr(const ScopePtr& scope, const AstExprUnary& expr) { WithPredicate result = checkExpr(scope, *expr.expr); TypeId operandType = follow(result.type); switch (expr.op) { case AstExprUnary::Not: return {booleanType, {NotPredicate{std::move(result.predicates)}}}; case AstExprUnary::Minus: { const bool operandIsAny = get(operandType) || get(operandType); if (operandIsAny) return {operandType}; if (typeCouldHaveMetatable(operandType)) { if (auto fnt = findMetatableEntry(operandType, "__unm", expr.location)) { TypeId actualFunctionType = instantiate(scope, *fnt, expr.location); TypePackId arguments = addTypePack({operandType}); TypePackId retTypePack = freshTypePack(scope); TypeId expectedFunctionType = addType(FunctionTypeVar(scope->level, arguments, retTypePack)); Unifier state = mkUnifier(expr.location); state.tryUnify(actualFunctionType, expectedFunctionType, /*isFunctionCall*/ true); state.log.commit(); TypeId retType = first(retTypePack).value_or(nilType); if (!state.errors.empty()) retType = errorRecoveryType(retType); return {retType}; } reportError(expr.location, GenericError{format("Unary operator '%s' not supported by type '%s'", toString(expr.op).c_str(), toString(operandType).c_str())}); return {errorRecoveryType(scope)}; } reportErrors(tryUnify(operandType, numberType, expr.location)); return {numberType}; } case AstExprUnary::Len: { tablify(operandType); operandType = stripFromNilAndReport(operandType, expr.location); if (get(operandType)) return {errorRecoveryType(scope)}; DenseHashSet seen{nullptr}; if (!hasLength(operandType, seen, &recursionCount)) reportError(TypeError{expr.location, NotATable{operandType}}); return {numberType}; } default: ice("Unknown AstExprUnary " + std::to_string(int(expr.op))); } } std::string opToMetaTableEntry(const AstExprBinary::Op& op) { switch (op) { case AstExprBinary::CompareNe: case AstExprBinary::CompareEq: return "__eq"; case AstExprBinary::CompareLt: case AstExprBinary::CompareGe: return "__lt"; case AstExprBinary::CompareLe: case AstExprBinary::CompareGt: return "__le"; case AstExprBinary::Add: return "__add"; case AstExprBinary::Sub: return "__sub"; case AstExprBinary::Mul: return "__mul"; case AstExprBinary::Div: return "__div"; case AstExprBinary::Mod: return "__mod"; case AstExprBinary::Pow: return "__pow"; case AstExprBinary::Concat: return "__concat"; default: return ""; } } TypeId TypeChecker::unionOfTypes(TypeId a, TypeId b, const Location& location, bool unifyFreeTypes) { if (unifyFreeTypes && (get(a) || get(b))) { if (unify(b, a, location)) return a; return errorRecoveryType(anyType); } if (*a == *b) return a; std::vector types = reduceUnion({a, b}); if (types.size() == 1) return types[0]; return addType(UnionTypeVar{types}); } static std::optional getIdentifierOfBaseVar(AstExpr* node) { if (AstExprGlobal* expr = node->as()) return expr->name.value; if (AstExprLocal* expr = node->as()) return expr->local->name.value; if (AstExprIndexExpr* expr = node->as()) return getIdentifierOfBaseVar(expr->expr); if (AstExprIndexName* expr = node->as()) return getIdentifierOfBaseVar(expr->expr); return std::nullopt; } TypeId TypeChecker::checkRelationalOperation( const ScopePtr& scope, const AstExprBinary& expr, TypeId lhsType, TypeId rhsType, const PredicateVec& predicates) { auto stripNil = [this](TypeId ty, bool isOrOp = false) { ty = follow(ty); if (!isNonstrictMode() && !isOrOp) return ty; if (get(ty)) { std::optional cleaned = tryStripUnionFromNil(ty); // If there is no union option without 'nil' if (!cleaned) return nilType; return follow(*cleaned); } return follow(ty); }; bool isEquality = expr.op == AstExprBinary::CompareEq || expr.op == AstExprBinary::CompareNe; lhsType = stripNil(lhsType, expr.op == AstExprBinary::Or); rhsType = stripNil(rhsType); // If we know nothing at all about the lhs type, we can usually say nothing about the result. // The notable exception to this is the equality and inequality operators, which always produce a boolean. const bool lhsIsAny = get(lhsType) || get(lhsType); // Peephole check for `cond and a or b -> type(a)|type(b)` // TODO: Kill this when singleton types arrive. :( if (AstExprBinary* subexp = expr.left->as()) { if (expr.op == AstExprBinary::Or && subexp->op == AstExprBinary::And) { ScopePtr subScope = childScope(scope, subexp->location); resolve(predicates, subScope, true); return unionOfTypes(rhsType, stripNil(checkExpr(subScope, *subexp->right).type, true), expr.location); } } // Lua casts the results of these to boolean switch (expr.op) { case AstExprBinary::CompareNe: case AstExprBinary::CompareEq: { if (isNonstrictMode() && (isNil(lhsType) || isNil(rhsType))) return booleanType; const bool rhsIsAny = get(rhsType) || get(rhsType); if (lhsIsAny || rhsIsAny) return booleanType; // Fallthrough here is intentional } case AstExprBinary::CompareLt: case AstExprBinary::CompareGt: case AstExprBinary::CompareGe: case AstExprBinary::CompareLe: { /* Subtlety here: * We need to do this unification first, but there are situations where we don't actually want to * report any problems that might have been surfaced as a result of this step because we might already * have a better, more descriptive error teed up. */ Unifier state = mkUnifier(expr.location); if (!isEquality) { state.tryUnify(rhsType, lhsType); state.log.commit(); } bool needsMetamethod = !isEquality; TypeId leftType = follow(lhsType); if (get(leftType) || get(leftType) || get(leftType) || get(leftType)) { reportErrors(state.errors); if (!isEquality && state.errors.empty() && (get(leftType) || isBoolean(leftType))) { reportError(expr.location, GenericError{format("Type '%s' cannot be compared with relational operator %s", toString(leftType).c_str(), toString(expr.op).c_str())}); } return booleanType; } std::string metamethodName = opToMetaTableEntry(expr.op); std::optional leftMetatable = isString(lhsType) ? std::nullopt : getMetatable(follow(lhsType)); std::optional rightMetatable = isString(rhsType) ? std::nullopt : getMetatable(follow(rhsType)); if (leftMetatable != rightMetatable) { bool matches = false; if (isEquality) { if (const UnionTypeVar* utv = get(leftType); utv && rightMetatable) { for (TypeId leftOption : utv) { if (getMetatable(follow(leftOption)) == rightMetatable) { matches = true; break; } } } if (!matches) { if (const UnionTypeVar* utv = get(rhsType); utv && leftMetatable) { for (TypeId rightOption : utv) { if (getMetatable(follow(rightOption)) == leftMetatable) { matches = true; break; } } } } } if (!matches) { reportError( expr.location, GenericError{format("Types %s and %s cannot be compared with %s because they do not have the same metatable", toString(lhsType).c_str(), toString(rhsType).c_str(), toString(expr.op).c_str())}); return errorRecoveryType(booleanType); } } if (leftMetatable) { std::optional metamethod = findMetatableEntry(lhsType, metamethodName, expr.location); if (metamethod) { if (const FunctionTypeVar* ftv = get(*metamethod)) { if (isEquality) { Unifier state = mkUnifier(expr.location); state.tryUnify(addTypePack({booleanType}), ftv->retTypes); if (!state.errors.empty()) { reportError(expr.location, GenericError{format("Metamethod '%s' must return type 'boolean'", metamethodName.c_str())}); return errorRecoveryType(booleanType); } state.log.commit(); } } reportErrors(state.errors); TypeId actualFunctionType = addType(FunctionTypeVar(scope->level, addTypePack({lhsType, rhsType}), addTypePack({booleanType}))); state.tryUnify( instantiate(scope, actualFunctionType, expr.location), instantiate(scope, *metamethod, expr.location), /*isFunctionCall*/ true); state.log.commit(); reportErrors(state.errors); return booleanType; } else if (needsMetamethod) { reportError( expr.location, GenericError{format("Table %s does not offer metamethod %s", toString(lhsType).c_str(), metamethodName.c_str())}); return errorRecoveryType(booleanType); } } if (get(follow(lhsType)) && !isEquality) { auto name = getIdentifierOfBaseVar(expr.left); reportError(expr.location, CannotInferBinaryOperation{expr.op, name, CannotInferBinaryOperation::Comparison}); return errorRecoveryType(booleanType); } if (needsMetamethod) { reportError(expr.location, GenericError{format("Type %s cannot be compared with %s because it has no metatable", toString(lhsType).c_str(), toString(expr.op).c_str())}); return errorRecoveryType(booleanType); } return booleanType; } case AstExprBinary::And: if (lhsIsAny) return lhsType; return unionOfTypes(rhsType, booleanType, expr.location, false); case AstExprBinary::Or: if (lhsIsAny) return lhsType; return unionOfTypes(lhsType, rhsType, expr.location); default: LUAU_ASSERT(0); ice(format("checkRelationalOperation called with incorrect binary expression '%s'", toString(expr.op).c_str()), expr.location); } } TypeId TypeChecker::checkBinaryOperation( const ScopePtr& scope, const AstExprBinary& expr, TypeId lhsType, TypeId rhsType, const PredicateVec& predicates) { switch (expr.op) { case AstExprBinary::CompareNe: case AstExprBinary::CompareEq: case AstExprBinary::CompareLt: case AstExprBinary::CompareGt: case AstExprBinary::CompareGe: case AstExprBinary::CompareLe: case AstExprBinary::And: case AstExprBinary::Or: return checkRelationalOperation(scope, expr, lhsType, rhsType, predicates); default: break; } lhsType = follow(lhsType); rhsType = follow(rhsType); if (!isNonstrictMode() && get(lhsType)) { auto name = getIdentifierOfBaseVar(expr.left); reportError(expr.location, CannotInferBinaryOperation{expr.op, name, CannotInferBinaryOperation::Operation}); // We will fall-through to the `return anyType` check below. } // If we know nothing at all about the lhs type, we can usually say nothing about the result. // The notable exception to this is the equality and inequality operators, which always produce a boolean. const bool lhsIsAny = get(lhsType) || get(lhsType); const bool rhsIsAny = get(rhsType) || get(rhsType); if (lhsIsAny) return lhsType; if (rhsIsAny) return rhsType; if (get(lhsType)) { // Inferring this accurately will get a bit weird. // If the lhs type is not known, it could be assumed that it is a table or class that has a metatable // that defines the required method, but we don't know which. // For now, we'll give up and hope for the best. return anyType; } if (get(rhsType)) unify(rhsType, lhsType, expr.location); if (typeCouldHaveMetatable(lhsType) || typeCouldHaveMetatable(rhsType)) { auto checkMetatableCall = [this, &scope, &expr](TypeId fnt, TypeId lhst, TypeId rhst) -> TypeId { TypeId actualFunctionType = instantiate(scope, fnt, expr.location); TypePackId arguments = addTypePack({lhst, rhst}); TypePackId retTypePack = freshTypePack(scope); TypeId expectedFunctionType = addType(FunctionTypeVar(scope->level, arguments, retTypePack)); Unifier state = mkUnifier(expr.location); state.tryUnify(actualFunctionType, expectedFunctionType, /*isFunctionCall*/ true); reportErrors(state.errors); bool hasErrors = !state.errors.empty(); if (hasErrors) { // If there are unification errors, the return type may still be unknown // so we loosen the argument types to see if that helps. TypePackId fallbackArguments = freshTypePack(scope); TypeId fallbackFunctionType = addType(FunctionTypeVar(scope->level, fallbackArguments, retTypePack)); state.errors.clear(); state.log.clear(); state.tryUnify(actualFunctionType, fallbackFunctionType, /*isFunctionCall*/ true); if (state.errors.empty()) state.log.commit(); } else { state.log.commit(); } TypeId retType = first(retTypePack).value_or(nilType); if (hasErrors) retType = errorRecoveryType(retType); return retType; }; std::string op = opToMetaTableEntry(expr.op); if (auto fnt = findMetatableEntry(lhsType, op, expr.location)) return checkMetatableCall(*fnt, lhsType, rhsType); if (auto fnt = findMetatableEntry(rhsType, op, expr.location)) { // Note the intentionally reversed arguments here. return checkMetatableCall(*fnt, rhsType, lhsType); } reportError(expr.location, GenericError{format("Binary operator '%s' not supported by types '%s' and '%s'", toString(expr.op).c_str(), toString(lhsType).c_str(), toString(rhsType).c_str())}); return errorRecoveryType(scope); } switch (expr.op) { case AstExprBinary::Concat: reportErrors(tryUnify(lhsType, addType(UnionTypeVar{{stringType, numberType}}), expr.left->location)); reportErrors(tryUnify(rhsType, addType(UnionTypeVar{{stringType, numberType}}), expr.right->location)); return stringType; case AstExprBinary::Add: case AstExprBinary::Sub: case AstExprBinary::Mul: case AstExprBinary::Div: case AstExprBinary::Mod: case AstExprBinary::Pow: reportErrors(tryUnify(lhsType, numberType, expr.left->location)); reportErrors(tryUnify(rhsType, numberType, expr.right->location)); return numberType; default: // These should have been handled with checkRelationalOperation LUAU_ASSERT(0); return anyType; } } WithPredicate TypeChecker::checkExpr(const ScopePtr& scope, const AstExprBinary& expr) { if (expr.op == AstExprBinary::And) { auto [lhsTy, lhsPredicates] = checkExpr(scope, *expr.left); ScopePtr innerScope = childScope(scope, expr.location); resolve(lhsPredicates, innerScope, true); auto [rhsTy, rhsPredicates] = checkExpr(innerScope, *expr.right); return {checkBinaryOperation(scope, expr, lhsTy, rhsTy), {AndPredicate{std::move(lhsPredicates), std::move(rhsPredicates)}}}; } else if (expr.op == AstExprBinary::Or) { auto [lhsTy, lhsPredicates] = checkExpr(scope, *expr.left); ScopePtr innerScope = childScope(scope, expr.location); resolve(lhsPredicates, innerScope, false); auto [rhsTy, rhsPredicates] = checkExpr(innerScope, *expr.right); // Because of C++, I'm not sure if lhsPredicates was not moved out by the time we call checkBinaryOperation. TypeId result = checkBinaryOperation(scope, expr, lhsTy, rhsTy, lhsPredicates); return {result, {OrPredicate{std::move(lhsPredicates), std::move(rhsPredicates)}}}; } else if (expr.op == AstExprBinary::CompareEq || expr.op == AstExprBinary::CompareNe) { if (auto predicate = tryGetTypeGuardPredicate(expr)) return {booleanType, {std::move(*predicate)}}; WithPredicate lhs = checkExpr(scope, *expr.left, std::nullopt, /*forceSingleton=*/true); WithPredicate rhs = checkExpr(scope, *expr.right, std::nullopt, /*forceSingleton=*/true); PredicateVec predicates; if (auto lvalue = tryGetLValue(*expr.left)) predicates.push_back(EqPredicate{std::move(*lvalue), rhs.type, expr.location}); if (auto lvalue = tryGetLValue(*expr.right)) predicates.push_back(EqPredicate{std::move(*lvalue), lhs.type, expr.location}); if (!predicates.empty() && expr.op == AstExprBinary::CompareNe) predicates = {NotPredicate{std::move(predicates)}}; return {checkBinaryOperation(scope, expr, lhs.type, rhs.type), std::move(predicates)}; } else { WithPredicate lhs = checkExpr(scope, *expr.left); WithPredicate rhs = checkExpr(scope, *expr.right); // Intentionally discarding predicates with other operators. return {checkBinaryOperation(scope, expr, lhs.type, rhs.type, lhs.predicates)}; } } WithPredicate TypeChecker::checkExpr(const ScopePtr& scope, const AstExprTypeAssertion& expr) { TypeId annotationType = resolveType(scope, *expr.annotation); WithPredicate result = checkExpr(scope, *expr.expr, annotationType); // Note: As an optimization, we try 'number <: number | string' first, as that is the more likely case. if (canUnify(annotationType, result.type, expr.location).empty()) return {annotationType, std::move(result.predicates)}; if (canUnify(result.type, annotationType, expr.location).empty()) return {annotationType, std::move(result.predicates)}; reportError(expr.location, TypesAreUnrelated{result.type, annotationType}); return {errorRecoveryType(annotationType), std::move(result.predicates)}; } WithPredicate TypeChecker::checkExpr(const ScopePtr& scope, const AstExprError& expr) { const size_t oldSize = currentModule->errors.size(); for (AstExpr* expr : expr.expressions) checkExpr(scope, *expr); // HACK: We want to check the contents of the AstExprError, but // any type errors that may arise from it are going to be useless. currentModule->errors.resize(oldSize); return {errorRecoveryType(scope)}; } WithPredicate TypeChecker::checkExpr(const ScopePtr& scope, const AstExprIfElse& expr, std::optional expectedType) { WithPredicate result = checkExpr(scope, *expr.condition); ScopePtr trueScope = childScope(scope, expr.trueExpr->location); resolve(result.predicates, trueScope, true); WithPredicate trueType = checkExpr(trueScope, *expr.trueExpr, expectedType); ScopePtr falseScope = childScope(scope, expr.falseExpr->location); resolve(result.predicates, falseScope, false); WithPredicate falseType = checkExpr(falseScope, *expr.falseExpr, expectedType); if (falseType.type == trueType.type) return {trueType.type}; std::vector types = reduceUnion({trueType.type, falseType.type}); return {types.size() == 1 ? types[0] : addType(UnionTypeVar{std::move(types)})}; } TypeId TypeChecker::checkLValue(const ScopePtr& scope, const AstExpr& expr) { return checkLValueBinding(scope, expr); } TypeId TypeChecker::checkLValueBinding(const ScopePtr& scope, const AstExpr& expr) { if (auto a = expr.as()) return checkLValueBinding(scope, *a); else if (auto a = expr.as()) return checkLValueBinding(scope, *a); else if (auto a = expr.as()) return checkLValueBinding(scope, *a); else if (auto a = expr.as()) return checkLValueBinding(scope, *a); else if (auto a = expr.as()) { for (AstExpr* expr : a->expressions) checkExpr(scope, *expr); return errorRecoveryType(scope); } else ice("Unexpected AST node in checkLValue", expr.location); } TypeId TypeChecker::checkLValueBinding(const ScopePtr& scope, const AstExprLocal& expr) { if (std::optional ty = scope->lookup(expr.local)) return *ty; reportError(expr.location, UnknownSymbol{expr.local->name.value, UnknownSymbol::Binding}); return errorRecoveryType(scope); } TypeId TypeChecker::checkLValueBinding(const ScopePtr& scope, const AstExprGlobal& expr) { Name name = expr.name.value; ScopePtr moduleScope = currentModule->getModuleScope(); const auto it = moduleScope->bindings.find(expr.name); if (it != moduleScope->bindings.end()) return it->second.typeId; TypeId result = freshType(scope); Binding& binding = moduleScope->bindings[expr.name]; binding = {result, expr.location}; // If we're in strict mode, we want to report defining a global as an error, // but still add it to the bindings, so that autocomplete includes it in completions. if (!isNonstrictMode()) reportError(TypeError{expr.location, UnknownSymbol{name, UnknownSymbol::Binding}}); return result; } TypeId TypeChecker::checkLValueBinding(const ScopePtr& scope, const AstExprIndexName& expr) { TypeId lhs = checkExpr(scope, *expr.expr).type; if (get(lhs) || get(lhs)) return lhs; tablify(lhs); Name name = expr.index.value; lhs = stripFromNilAndReport(lhs, expr.expr->location); if (TableTypeVar* lhsTable = getMutableTableType(lhs)) { const auto& it = lhsTable->props.find(name); if (it != lhsTable->props.end()) { return it->second.type; } else if (lhsTable->state == TableState::Unsealed || lhsTable->state == TableState::Free) { TypeId theType = freshType(scope); Property& property = lhsTable->props[name]; property.type = theType; property.location = expr.indexLocation; return theType; } else if (auto indexer = lhsTable->indexer) { Unifier state = mkUnifier(expr.location); state.tryUnify(stringType, indexer->indexType); TypeId retType = indexer->indexResultType; if (!state.errors.empty()) { reportError(expr.location, UnknownProperty{lhs, name}); retType = errorRecoveryType(retType); } else state.log.commit(); return retType; } else if (lhsTable->state == TableState::Sealed) { reportError(TypeError{expr.location, CannotExtendTable{lhs, CannotExtendTable::Property, name}}); return errorRecoveryType(scope); } else { reportError(TypeError{expr.location, GenericError{"Internal error: generic tables are not lvalues"}}); return errorRecoveryType(scope); } } else if (const ClassTypeVar* lhsClass = get(lhs)) { const Property* prop = lookupClassProp(lhsClass, name); if (!prop) { reportError(TypeError{expr.location, UnknownProperty{lhs, name}}); return errorRecoveryType(scope); } return prop->type; } else if (get(lhs)) { if (std::optional ty = getIndexTypeFromType(scope, lhs, name, expr.location, false)) return *ty; // If intersection has a table part, report that it cannot be extended just as a sealed table if (isTableIntersection(lhs)) { reportError(TypeError{expr.location, CannotExtendTable{lhs, CannotExtendTable::Property, name}}); return errorRecoveryType(scope); } } reportError(TypeError{expr.location, NotATable{lhs}}); return errorRecoveryType(scope); } TypeId TypeChecker::checkLValueBinding(const ScopePtr& scope, const AstExprIndexExpr& expr) { TypeId exprType = checkExpr(scope, *expr.expr).type; tablify(exprType); exprType = stripFromNilAndReport(exprType, expr.expr->location); TypeId indexType = checkExpr(scope, *expr.index).type; if (get(exprType) || get(exprType)) return exprType; AstExprConstantString* value = expr.index->as(); if (value) { if (const ClassTypeVar* exprClass = get(exprType)) { const Property* prop = lookupClassProp(exprClass, value->value.data); if (!prop) { reportError(TypeError{expr.location, UnknownProperty{exprType, value->value.data}}); return errorRecoveryType(scope); } return prop->type; } } TableTypeVar* exprTable = getMutableTableType(exprType); if (!exprTable) { reportError(TypeError{expr.expr->location, NotATable{exprType}}); return errorRecoveryType(scope); } if (value) { const auto& it = exprTable->props.find(value->value.data); if (it != exprTable->props.end()) { return it->second.type; } else if (exprTable->state == TableState::Unsealed || exprTable->state == TableState::Free) { TypeId resultType = freshType(scope); Property& property = exprTable->props[value->value.data]; property.type = resultType; property.location = expr.index->location; return resultType; } } if (exprTable->indexer) { const TableIndexer& indexer = *exprTable->indexer; unify(indexType, indexer.indexType, expr.index->location); return indexer.indexResultType; } else if (exprTable->state == TableState::Unsealed || exprTable->state == TableState::Free) { TypeId resultType = freshType(exprTable->level); exprTable->indexer = TableIndexer{anyIfNonstrict(indexType), anyIfNonstrict(resultType)}; return resultType; } else { /* * If we use [] indexing to fetch a property from a sealed table that has no indexer, we have no idea if it will * work, so we just mint a fresh type, return that, and hope for the best. */ TypeId resultType = freshType(scope); return resultType; } } // Answers the question: "Can I define another function with this name?" // Primarily about detecting duplicates. TypeId TypeChecker::checkFunctionName(const ScopePtr& scope, AstExpr& funName, TypeLevel level) { auto freshTy = [&]() { return freshType(level); }; if (auto globalName = funName.as()) { const ScopePtr& globalScope = currentModule->getModuleScope(); Symbol name = globalName->name; if (globalScope->bindings.count(name)) { if (isNonstrictMode()) return globalScope->bindings[name].typeId; return errorRecoveryType(scope); } else { TypeId ty = freshTy(); globalScope->bindings[name] = {ty, funName.location}; return ty; } } else if (auto localName = funName.as()) { Symbol name = localName->local; Binding& binding = scope->bindings[name]; if (binding.typeId == nullptr) binding = {freshTy(), funName.location}; return binding.typeId; } else if (auto indexName = funName.as()) { TypeId lhsType = checkExpr(scope, *indexName->expr).type; TableTypeVar* ttv = getMutableTableType(lhsType); if (!ttv || ttv->state == TableState::Sealed) { if (auto ty = getIndexTypeFromType(scope, lhsType, indexName->index.value, indexName->indexLocation, false)) return *ty; return errorRecoveryType(scope); } Name name = indexName->index.value; if (ttv->props.count(name)) return ttv->props[name].type; Property& property = ttv->props[name]; property.type = freshTy(); property.location = indexName->indexLocation; return property.type; } else if (funName.is()) return errorRecoveryType(scope); else { ice("Unexpected AST node type", funName.location); } } // This returns a pair `[funType, funScope]` where // - funType is the prototype type of the function // - funScope is the scope for the function, which is a child scope with bindings added for // parameters (and generic types if there were explicit generic annotations). // // The function type is a prototype, in that it may be missing some generic types which // can only be inferred from type inference after typechecking the function body. // For example the function `function id(x) return x end` has prototype // `(X) -> Y...`, but after typechecking the body, we cam unify `Y...` with `X` // to get type `(X) -> X`, then we quantify the free types to get the final // generic type `(a) -> a`. std::pair TypeChecker::checkFunctionSignature( const ScopePtr& scope, int subLevel, const AstExprFunction& expr, std::optional originalName, std::optional expectedType) { ScopePtr funScope = childFunctionScope(scope, expr.location, subLevel); const FunctionTypeVar* expectedFunctionType = nullptr; if (expectedType) { LUAU_ASSERT(!expr.self); if (auto ftv = get(follow(*expectedType))) { expectedFunctionType = ftv; } else if (auto utv = get(follow(*expectedType))) { // Look for function type in a union. Other types can be ignored since current expression is a function for (auto option : utv) { if (auto ftv = get(follow(option))) { if (!expectedFunctionType) { expectedFunctionType = ftv; } else { // Do not infer argument types when multiple overloads are expected expectedFunctionType = nullptr; break; } } } } // We do not infer type binders, so if a generic function is required we do not propagate if (expectedFunctionType && !(expectedFunctionType->generics.empty() && expectedFunctionType->genericPacks.empty())) expectedFunctionType = nullptr; } auto [generics, genericPacks] = createGenericTypes(funScope, std::nullopt, expr, expr.generics, expr.genericPacks); TypePackId retPack; if (expr.returnAnnotation) retPack = resolveTypePack(funScope, *expr.returnAnnotation); else if (FFlag::LuauReturnTypeInferenceInNonstrict ? (!FFlag::LuauLowerBoundsCalculation && isNonstrictMode()) : isNonstrictMode()) retPack = anyTypePack; else if (expectedFunctionType) { auto [head, tail] = flatten(expectedFunctionType->retTypes); // Do not infer 'nil' as function return type if (!tail && head.size() == 1 && isNil(head[0])) retPack = freshTypePack(funScope); else retPack = addTypePack(head, tail); } else retPack = freshTypePack(funScope); if (expr.vararg) { if (expr.varargAnnotation) funScope->varargPack = resolveTypePack(funScope, *expr.varargAnnotation); else { if (expectedFunctionType && !isNonstrictMode()) { auto [head, tail] = flatten(expectedFunctionType->argTypes); if (expr.args.size <= head.size()) { head.erase(head.begin(), head.begin() + expr.args.size); funScope->varargPack = addTypePack(head, tail); } else if (tail) { if (get(follow(*tail))) funScope->varargPack = addTypePack({}, tail); } else { funScope->varargPack = addTypePack({}); } } // TODO: should this be a free type pack? CLI-39910 if (!funScope->varargPack) funScope->varargPack = anyTypePack; } } else if (FFlag::LuauLowerBoundsCalculation && !isNonstrictMode()) { funScope->varargPack = addTypePack(TypePackVar{VariadicTypePack{anyType, /*hidden*/ true}}); } std::vector argTypes; funScope->returnType = retPack; if (expr.self) { // TODO: generic self types: CLI-39906 TypeId selfType = anyIfNonstrict(freshType(funScope)); funScope->bindings[expr.self] = {selfType, expr.self->location}; argTypes.push_back(selfType); } // Prepare expected argument type iterators if we have an expected function type TypePackIterator expectedArgsCurr, expectedArgsEnd; if (expectedFunctionType && !isNonstrictMode()) { expectedArgsCurr = begin(expectedFunctionType->argTypes); expectedArgsEnd = end(expectedFunctionType->argTypes); } for (AstLocal* local : expr.args) { TypeId argType = nullptr; if (local->annotation) { argType = resolveType(funScope, *local->annotation); // If the annotation type has an error, treat it as if there was no annotation if (get(follow(argType))) argType = anyIfNonstrict(freshType(funScope)); } else { if (expectedFunctionType && !isNonstrictMode()) { if (expectedArgsCurr != expectedArgsEnd) { argType = *expectedArgsCurr; } else if (auto expectedArgsTail = expectedArgsCurr.tail()) { if (const VariadicTypePack* vtp = get(follow(*expectedArgsTail))) argType = vtp->ty; } } if (!argType) argType = anyIfNonstrict(freshType(funScope)); } funScope->bindings[local] = {argType, local->location}; argTypes.push_back(argType); if (expectedArgsCurr != expectedArgsEnd) ++expectedArgsCurr; } TypePackId argPack = addTypePack(TypePackVar(TypePack{argTypes, funScope->varargPack})); FunctionDefinition defn; defn.definitionModuleName = currentModuleName; defn.definitionLocation = expr.location; defn.varargLocation = expr.vararg ? std::make_optional(expr.varargLocation) : std::nullopt; defn.originalNameLocation = originalName.value_or(Location(expr.location.begin, 0)); std::vector genericTys; genericTys.reserve(generics.size()); std::transform(generics.begin(), generics.end(), std::back_inserter(genericTys), [](auto&& el) { return el.ty; }); std::vector genericTps; genericTps.reserve(genericPacks.size()); std::transform(genericPacks.begin(), genericPacks.end(), std::back_inserter(genericTps), [](auto&& el) { return el.tp; }); TypeId funTy = addType(FunctionTypeVar(funScope->level, std::move(genericTys), std::move(genericTps), argPack, retPack, std::move(defn), bool(expr.self))); FunctionTypeVar* ftv = getMutable(funTy); ftv->argNames.reserve(expr.args.size + (expr.self ? 1 : 0)); if (expr.self) ftv->argNames.push_back(FunctionArgument{"self", {}}); for (AstLocal* local : expr.args) ftv->argNames.push_back(FunctionArgument{local->name.value, local->location}); return std::make_pair(funTy, funScope); } static bool allowsNoReturnValues(const TypePackId tp) { for (TypeId ty : tp) { if (!get(follow(ty))) { return false; } } return true; } static Location getEndLocation(const AstExprFunction& function) { Location loc = function.location; if (loc.begin.line != loc.end.line) { Position begin = loc.end; begin.column = std::max(0u, begin.column - 3); loc = Location(begin, 3); } return loc; } void TypeChecker::checkFunctionBody(const ScopePtr& scope, TypeId ty, const AstExprFunction& function) { LUAU_TIMETRACE_SCOPE("TypeChecker::checkFunctionBody", "TypeChecker"); if (function.debugname.value) LUAU_TIMETRACE_ARGUMENT("name", function.debugname.value); else LUAU_TIMETRACE_ARGUMENT("line", std::to_string(function.location.begin.line).c_str()); if (FunctionTypeVar* funTy = getMutable(ty)) { check(scope, *function.body); if (useConstrainedIntersections()) { TypePackId retPack = follow(funTy->retTypes); // It is possible for a function to have no annotation and no return statement, and yet still have an ascribed return type // if it is expected to conform to some other interface. (eg the function may be a lambda passed as a callback) if (!hasReturn(function.body) && !function.returnAnnotation.has_value() && get(retPack)) { auto level = getLevel(retPack); if (level && scope->level.subsumes(*level)) *asMutable(retPack) = TypePack{{}, std::nullopt}; } } else { // We explicitly don't follow here to check if we have a 'true' free type instead of bound one if (get_if(&funTy->retTypes->ty)) *asMutable(funTy->retTypes) = TypePack{{}, std::nullopt}; } bool reachesImplicitReturn = getFallthrough(function.body) != nullptr; if (reachesImplicitReturn && !allowsNoReturnValues(follow(funTy->retTypes))) { // If we're in nonstrict mode we want to only report this missing return // statement if there are type annotations on the function. In strict mode // we report it regardless. if (!isNonstrictMode() || function.returnAnnotation) { reportError(getEndLocation(function), FunctionExitsWithoutReturning{funTy->retTypes}); } } } else ice("Checking non functional type"); } WithPredicate TypeChecker::checkExprPack(const ScopePtr& scope, const AstExpr& expr) { if (auto a = expr.as()) return checkExprPack(scope, *a); else if (expr.is()) { if (!scope->varargPack) return {errorRecoveryTypePack(scope)}; return {*scope->varargPack}; } else { TypeId type = checkExpr(scope, expr).type; return {addTypePack({type})}; } } // Returns the minimum number of arguments the argument list can accept. static size_t getMinParameterCount(TxnLog* log, TypePackId tp) { size_t minCount = 0; size_t optionalCount = 0; auto it = begin(tp, log); auto endIter = end(tp); while (it != endIter) { TypeId ty = *it; if (isOptional(ty)) ++optionalCount; else { minCount += optionalCount; optionalCount = 0; minCount++; } ++it; } return minCount; } void TypeChecker::checkArgumentList( const ScopePtr& scope, Unifier& state, TypePackId argPack, TypePackId paramPack, const std::vector& argLocations) { /* Important terminology refresher: * A function requires parameters. * To call a function, you supply arguments. */ TypePackIterator argIter = begin(argPack, &state.log); TypePackIterator paramIter = begin(paramPack, &state.log); TypePackIterator endIter = end(argPack); // Important subtlety: All end TypePackIterators are equivalent size_t paramIndex = 0; auto reportCountMismatchError = [&state, &argLocations, paramPack, argPack]() { // For this case, we want the error span to cover every errant extra parameter Location location = state.location; if (!argLocations.empty()) location = {state.location.begin, argLocations.back().end}; size_t minParams = getMinParameterCount(&state.log, paramPack); state.reportError(TypeError{location, CountMismatch{minParams, std::distance(begin(argPack), end(argPack))}}); }; while (true) { state.location = paramIndex < argLocations.size() ? argLocations[paramIndex] : state.location; if (argIter == endIter && paramIter == endIter) { std::optional argTail = argIter.tail(); std::optional paramTail = paramIter.tail(); // If we hit the end of both type packs simultaneously, then there are definitely no further type // errors to report. All we need to do is tie up any free tails. // // If one side has a free tail and the other has none at all, we create an empty pack and bind the // free tail to that. if (argTail) { if (state.log.getMutable(state.log.follow(*argTail))) { if (paramTail) state.tryUnify(*paramTail, *argTail); else state.log.replace(*argTail, TypePackVar(TypePack{{}})); } } else if (paramTail) { // argTail is definitely empty if (state.log.getMutable(state.log.follow(*paramTail))) state.log.replace(*paramTail, TypePackVar(TypePack{{}})); } return; } else if (argIter == endIter) { // Not enough arguments. // Might be ok if we are forwarding a vararg along. This is a common thing to occur in nonstrict mode. if (argIter.tail()) { TypePackId tail = *argIter.tail(); if (state.log.getMutable(tail)) { // Unify remaining parameters so we don't leave any free-types hanging around. while (paramIter != endIter) { state.tryUnify(errorRecoveryType(anyType), *paramIter); ++paramIter; } return; } else if (auto vtp = state.log.getMutable(tail)) { // Function is variadic and requires that all subsequent parameters // be compatible with a type. while (paramIter != endIter) { state.tryUnify(vtp->ty, *paramIter); ++paramIter; } return; } else if (state.log.getMutable(tail)) { std::vector rest; rest.reserve(std::distance(paramIter, endIter)); while (paramIter != endIter) { rest.push_back(*paramIter); ++paramIter; } TypePackId varPack = addTypePack(TypePackVar{TypePack{rest, paramIter.tail()}}); state.tryUnify(varPack, tail); return; } } // If any remaining unfulfilled parameters are nonoptional, this is a problem. while (paramIter != endIter) { TypeId t = state.log.follow(*paramIter); if (isOptional(t)) { } // ok else if (state.log.getMutable(t)) { } // ok else { size_t minParams = getMinParameterCount(&state.log, paramPack); std::optional tail = flatten(paramPack, state.log).second; bool isVariadic = tail && Luau::isVariadic(*tail); state.reportError(TypeError{state.location, CountMismatch{minParams, paramIndex, CountMismatch::Context::Arg, isVariadic}}); return; } ++paramIter; } } else if (paramIter == endIter) { // too many parameters passed if (!paramIter.tail()) { while (argIter != endIter) { // The use of unify here is deliberate. We don't want this unification // to be undoable. unify(errorRecoveryType(scope), *argIter, state.location); ++argIter; } reportCountMismatchError(); return; } TypePackId tail = state.log.follow(*paramIter.tail()); if (state.log.getMutable(tail)) { // Function is variadic. Ok. return; } else if (auto vtp = state.log.getMutable(tail)) { if (FFlag::LuauLowerBoundsCalculation && vtp->hidden) { // We know that this function can technically be oversaturated, but we have its definition and we // know that it's useless. TypeId e = errorRecoveryType(scope); while (argIter != endIter) { unify(e, *argIter, state.location); ++argIter; } reportCountMismatchError(); return; } // Function is variadic and requires that all subsequent parameters // be compatible with a type. size_t argIndex = paramIndex; while (argIter != endIter) { Location location = state.location; if (argIndex < argLocations.size()) location = argLocations[argIndex]; unify(*argIter, vtp->ty, location); ++argIter; ++argIndex; } return; } else if (state.log.getMutable(tail)) { // Create a type pack out of the remaining argument types // and unify it with the tail. std::vector rest; rest.reserve(std::distance(argIter, endIter)); while (argIter != endIter) { rest.push_back(*argIter); ++argIter; } TypePackId varPack = addTypePack(TypePackVar{TypePack{rest, argIter.tail()}}); state.tryUnify(varPack, tail); return; } else if (state.log.getMutable(tail)) { state.log.replace(tail, TypePackVar(TypePack{{}})); return; } else if (state.log.getMutable(tail)) { reportCountMismatchError(); return; } } else { unifyWithInstantiationIfNeeded(scope, *argIter, *paramIter, state); ++argIter; ++paramIter; } ++paramIndex; } } WithPredicate TypeChecker::checkExprPack(const ScopePtr& scope, const AstExprCall& expr) { // evaluate type of function // decompose an intersection into its component overloads // Compute types of parameters // For each overload // Compare parameter and argument types // Report any errors (also speculate dot vs colon warnings!) // Return the resulting return type (even if there are errors) // If there are no matching overloads, unify with (a...) -> (b...) and return b... TypeId selfType = nullptr; TypeId functionType = nullptr; TypeId actualFunctionType = nullptr; if (expr.self) { AstExprIndexName* indexExpr = expr.func->as(); if (!indexExpr) ice("method call expression has no 'self'"); selfType = checkExpr(scope, *indexExpr->expr).type; selfType = stripFromNilAndReport(selfType, expr.func->location); if (std::optional propTy = getIndexTypeFromType(scope, selfType, indexExpr->index.value, expr.location, true)) { functionType = *propTy; actualFunctionType = instantiate(scope, functionType, expr.func->location); } else { functionType = errorRecoveryType(scope); actualFunctionType = functionType; } } else { functionType = checkExpr(scope, *expr.func).type; actualFunctionType = instantiate(scope, functionType, expr.func->location); } TypePackId retPack; if (FFlag::LuauLowerBoundsCalculation) { retPack = freshTypePack(scope->level); } else { if (auto free = get(actualFunctionType)) { retPack = freshTypePack(free->level); TypePackId freshArgPack = freshTypePack(free->level); asMutable(actualFunctionType)->ty.emplace(free->level, freshArgPack, retPack); } else retPack = freshTypePack(scope->level); } // checkExpr will log the pre-instantiated type of the function. // That's not nearly as interesting as the instantiated type, which will include details about how // generic functions are being instantiated for this particular callsite. currentModule->astOriginalCallTypes[expr.func] = follow(functionType); currentModule->astTypes[expr.func] = actualFunctionType; std::vector overloads = flattenIntersection(actualFunctionType); std::vector> expectedTypes = getExpectedTypesForCall(overloads, expr.args.size, expr.self); WithPredicate argListResult = checkExprList(scope, expr.location, expr.args, false, {}, expectedTypes); TypePackId argPack = argListResult.type; if (get(argPack)) return {errorRecoveryTypePack(scope)}; TypePack* args = getMutable(argPack); LUAU_ASSERT(args != nullptr); if (expr.self) args->head.insert(args->head.begin(), selfType); std::vector argLocations; argLocations.reserve(expr.args.size + 1); if (expr.self) argLocations.push_back(expr.func->as()->expr->location); for (AstExpr* arg : expr.args) argLocations.push_back(arg->location); std::vector errors; // errors encountered for each overload std::vector overloadsThatMatchArgCount; std::vector overloadsThatDont; for (TypeId fn : overloads) { fn = follow(fn); if (auto ret = checkCallOverload( scope, expr, fn, retPack, argPack, args, &argLocations, argListResult, overloadsThatMatchArgCount, overloadsThatDont, errors)) return *ret; } if (handleSelfCallMismatch(scope, expr, args, argLocations, errors)) return {retPack}; reportOverloadResolutionError(scope, expr, retPack, argPack, argLocations, overloads, overloadsThatMatchArgCount, errors); const FunctionTypeVar* overload = nullptr; if (!overloadsThatMatchArgCount.empty()) overload = get(overloadsThatMatchArgCount[0]); if (!overload && !overloadsThatDont.empty()) overload = get(overloadsThatDont[0]); if (overload) return {errorRecoveryTypePack(overload->retTypes)}; return {errorRecoveryTypePack(retPack)}; } std::vector> TypeChecker::getExpectedTypesForCall(const std::vector& overloads, size_t argumentCount, bool selfCall) { std::vector> expectedTypes; auto assignOption = [this, &expectedTypes](size_t index, TypeId ty) { if (index == expectedTypes.size()) { expectedTypes.push_back(ty); } else if (ty) { auto& el = expectedTypes[index]; if (!el) { el = ty; } else { std::vector result = reduceUnion({*el, ty}); el = result.size() == 1 ? result[0] : addType(UnionTypeVar{std::move(result)}); } } }; for (const TypeId overload : overloads) { if (const FunctionTypeVar* ftv = get(overload)) { auto [argsHead, argsTail] = flatten(ftv->argTypes); size_t start = selfCall ? 1 : 0; size_t index = 0; for (size_t i = start; i < argsHead.size(); ++i) assignOption(index++, argsHead[i]); if (argsTail) { argsTail = follow(*argsTail); if (const VariadicTypePack* vtp = get(*argsTail)) { while (index < argumentCount) assignOption(index++, vtp->ty); } } } } Demoter demoter{¤tModule->internalTypes}; demoter.demote(expectedTypes); return expectedTypes; } std::optional> TypeChecker::checkCallOverload(const ScopePtr& scope, const AstExprCall& expr, TypeId fn, TypePackId retPack, TypePackId argPack, TypePack* args, const std::vector* argLocations, const WithPredicate& argListResult, std::vector& overloadsThatMatchArgCount, std::vector& overloadsThatDont, std::vector& errors) { LUAU_ASSERT(argLocations); fn = stripFromNilAndReport(fn, expr.func->location); if (get(fn)) { unify(anyTypePack, argPack, expr.location); return {{anyTypePack}}; } if (get(fn)) { return {{errorRecoveryTypePack(scope)}}; } if (auto ftv = get(fn)) { // fn is one of the overloads of actualFunctionType, which // has been instantiated, so is a monotype. We can therefore // unify it with a monomorphic function. if (useConstrainedIntersections()) { // This ternary is phrased deliberately. We need ties between sibling scopes to bias toward ftv->level. const TypeLevel level = scope->level.subsumes(ftv->level) ? scope->level : ftv->level; std::vector adjustedArgTypes; auto it = begin(argPack); auto endIt = end(argPack); Widen widen{¤tModule->internalTypes}; for (; it != endIt; ++it) { adjustedArgTypes.push_back(addType(ConstrainedTypeVar{level, {widen(*it)}})); } TypePackId adjustedArgPack = addTypePack(TypePack{std::move(adjustedArgTypes), it.tail()}); TxnLog log; promoteTypeLevels(log, ¤tModule->internalTypes, level, retPack); log.commit(); *asMutable(fn) = FunctionTypeVar{level, adjustedArgPack, retPack}; return {{retPack}}; } else { TypeId r = addType(FunctionTypeVar(scope->level, argPack, retPack)); UnifierOptions options; options.isFunctionCall = true; unify(r, fn, expr.location, options); return {{retPack}}; } } std::vector metaArgLocations; // Might be a callable table if (const MetatableTypeVar* mttv = get(fn)) { if (std::optional ty = getIndexTypeFromType(scope, mttv->metatable, "__call", expr.func->location, false)) { // Construct arguments with 'self' added in front TypePackId metaCallArgPack = addTypePack(TypePackVar(TypePack{args->head, args->tail})); TypePack* metaCallArgs = getMutable(metaCallArgPack); metaCallArgs->head.insert(metaCallArgs->head.begin(), fn); metaArgLocations = *argLocations; metaArgLocations.insert(metaArgLocations.begin(), expr.func->location); fn = instantiate(scope, *ty, expr.func->location); argPack = metaCallArgPack; args = metaCallArgs; argLocations = &metaArgLocations; } } const FunctionTypeVar* ftv = get(fn); if (!ftv) { reportError(TypeError{expr.func->location, CannotCallNonFunction{fn}}); unify(errorRecoveryTypePack(scope), retPack, expr.func->location); return {{errorRecoveryTypePack(retPack)}}; } // When this function type has magic functions and did return something, we select that overload instead. // TODO: pass in a Unifier object to the magic functions? This will allow the magic functions to cooperate with overload resolution. if (ftv->magicFunction) { // TODO: We're passing in the wrong TypePackId. Should be argPack, but a unit test fails otherwise. CLI-40458 if (std::optional> ret = ftv->magicFunction(*this, scope, expr, argListResult)) return *ret; } Unifier state = mkUnifier(expr.location); // Unify return types checkArgumentList(scope, state, retPack, ftv->retTypes, /*argLocations*/ {}); if (!state.errors.empty()) { return {}; } checkArgumentList(scope, state, argPack, ftv->argTypes, *argLocations); if (!state.errors.empty()) { bool argMismatch = false; for (auto error : state.errors) { CountMismatch* cm = get(error); if (!cm) continue; if (cm->context == CountMismatch::Arg) { argMismatch = true; break; } } if (!argMismatch) overloadsThatMatchArgCount.push_back(fn); else overloadsThatDont.push_back(fn); errors.emplace_back(std::move(state.errors), args->head, ftv); } else { state.log.commit(); currentModule->astOverloadResolvedTypes[&expr] = fn; // We select this overload return {{retPack}}; } return {}; } bool TypeChecker::handleSelfCallMismatch(const ScopePtr& scope, const AstExprCall& expr, TypePack* args, const std::vector& argLocations, const std::vector& errors) { // No overloads succeeded: Scan for one that would have worked had the user // used a.b() rather than a:b() or vice versa. for (const auto& [_, argVec, ftv] : errors) { // Did you write foo:bar() when you should have written foo.bar()? if (expr.self) { std::vector editedArgLocations(argLocations.begin() + 1, argLocations.end()); std::vector editedParamList(args->head.begin() + 1, args->head.end()); TypePackId editedArgPack = addTypePack(TypePack{editedParamList}); Unifier editedState = mkUnifier(expr.location); checkArgumentList(scope, editedState, editedArgPack, ftv->argTypes, editedArgLocations); if (editedState.errors.empty()) { editedState.log.commit(); reportError(TypeError{expr.location, FunctionDoesNotTakeSelf{}}); // This is a little bit suspect: If this overload would work with a . replaced by a : // we eagerly assume that that's what you actually meant and we commit to it. // This could be incorrect if the function has an additional overload that // actually works. // checkArgumentList(scope, editedState, retPack, ftv->retTypes, retLocations, CountMismatch::Return); return true; } } else if (ftv->hasSelf) { // Did you write foo.bar() when you should have written foo:bar()? if (AstExprIndexName* indexName = expr.func->as()) { std::vector editedArgLocations; editedArgLocations.reserve(argLocations.size() + 1); editedArgLocations.push_back(indexName->expr->location); editedArgLocations.insert(editedArgLocations.end(), argLocations.begin(), argLocations.end()); std::vector editedArgList(args->head); editedArgList.insert(editedArgList.begin(), checkExpr(scope, *indexName->expr).type); TypePackId editedArgPack = addTypePack(TypePack{editedArgList}); Unifier editedState = mkUnifier(expr.location); checkArgumentList(scope, editedState, editedArgPack, ftv->argTypes, editedArgLocations); if (editedState.errors.empty()) { editedState.log.commit(); reportError(TypeError{expr.location, FunctionRequiresSelf{}}); // This is a little bit suspect: If this overload would work with a : replaced by a . // we eagerly assume that that's what you actually meant and we commit to it. // This could be incorrect if the function has an additional overload that // actually works. // checkArgumentList(scope, editedState, retPack, ftv->retTypes, retLocations, CountMismatch::Return); return true; } } } } return false; } void TypeChecker::reportOverloadResolutionError(const ScopePtr& scope, const AstExprCall& expr, TypePackId retPack, TypePackId argPack, const std::vector& argLocations, const std::vector& overloads, const std::vector& overloadsThatMatchArgCount, const std::vector& errors) { if (overloads.size() == 1) { reportErrors(std::get<0>(errors.front())); return; } std::vector overloadTypes = overloadsThatMatchArgCount; if (overloadsThatMatchArgCount.size() == 0) { reportError(TypeError{expr.location, GenericError{"No overload for function accepts " + std::to_string(size(argPack)) + " arguments."}}); // If no overloads match argument count, just list all overloads. overloadTypes = overloads; } else { // Report errors of the first argument-count-matching, but failing overload TypeId overload = overloadsThatMatchArgCount[0]; // Remove the overload we are reporting errors about, from the list of alternative overloadTypes.erase(std::remove(overloadTypes.begin(), overloadTypes.end(), overload), overloadTypes.end()); const FunctionTypeVar* ftv = get(overload); auto error = std::find_if(errors.begin(), errors.end(), [ftv](const OverloadErrorEntry& e) { return ftv == std::get<2>(e); }); LUAU_ASSERT(error != errors.end()); reportErrors(std::get<0>(*error)); // If only one overload matched, we don't need this error because we provided the previous errors. if (overloadsThatMatchArgCount.size() == 1) return; } std::string s; for (size_t i = 0; i < overloadTypes.size(); ++i) { TypeId overload = overloadTypes[i]; Unifier state = mkUnifier(expr.location); // Unify return types if (const FunctionTypeVar* ftv = get(overload)) { checkArgumentList(scope, state, retPack, ftv->retTypes, {}); checkArgumentList(scope, state, argPack, ftv->argTypes, argLocations); } if (state.errors.empty()) state.log.commit(); if (i > 0) s += "; "; if (i > 0 && i == overloadTypes.size() - 1) s += "and "; s += toString(overload); } if (overloadsThatMatchArgCount.size() == 0) reportError(expr.func->location, ExtraInformation{"Available overloads: " + s}); else reportError(expr.func->location, ExtraInformation{"Other overloads are also not viable: " + s}); // No viable overload return; } WithPredicate TypeChecker::checkExprList(const ScopePtr& scope, const Location& location, const AstArray& exprs, bool substituteFreeForNil, const std::vector& instantiateGenerics, const std::vector>& expectedTypes) { TypePackId pack = addTypePack(TypePack{}); PredicateVec predicates; // At the moment we will be pushing all predicate sets into this. Do we need some way to split them up? auto insert = [&predicates](PredicateVec& vec) { for (Predicate& c : vec) predicates.push_back(std::move(c)); }; if (exprs.size == 0) return {pack}; TypePack* tp = getMutable(pack); size_t lastIndex = exprs.size - 1; tp->head.reserve(lastIndex); Unifier state = mkUnifier(location); std::vector inverseLogs; for (size_t i = 0; i < exprs.size; ++i) { AstExpr* expr = exprs.data[i]; std::optional expectedType = i < expectedTypes.size() ? expectedTypes[i] : std::nullopt; if (i == lastIndex && (expr->is() || expr->is())) { auto [typePack, exprPredicates] = checkExprPack(scope, *expr); insert(exprPredicates); if (std::optional firstTy = first(typePack)) { if (!currentModule->astTypes.find(expr)) currentModule->astTypes[expr] = follow(*firstTy); } if (expectedType) currentModule->astExpectedTypes[expr] = *expectedType; tp->tail = typePack; } else { auto [type, exprPredicates] = checkExpr(scope, *expr, expectedType); insert(exprPredicates); TypeId actualType = substituteFreeForNil && expr->is() ? freshType(scope) : type; if (instantiateGenerics.size() > i && instantiateGenerics[i]) actualType = instantiate(scope, actualType, expr->location); if (expectedType) { state.tryUnify(actualType, *expectedType); // Ugly: In future iterations of the loop, we might need the state of the unification we // just performed. There's not a great way to pass that into checkExpr. Instead, we store // the inverse of the current log, and commit it. When we're done, we'll commit all the // inverses. This isn't optimal, and a better solution is welcome here. inverseLogs.push_back(state.log.inverse()); state.log.commit(); } tp->head.push_back(actualType); } } for (TxnLog& log : inverseLogs) log.commit(); return {pack, predicates}; } std::optional TypeChecker::matchRequire(const AstExprCall& call) { const char* require = "require"; if (call.args.size != 1) return std::nullopt; const AstExprGlobal* funcAsGlobal = call.func->as(); if (!funcAsGlobal || funcAsGlobal->name != require) return std::nullopt; if (call.args.size != 1) return std::nullopt; return call.args.data[0]; } TypeId TypeChecker::checkRequire(const ScopePtr& scope, const ModuleInfo& moduleInfo, const Location& location) { LUAU_TIMETRACE_SCOPE("TypeChecker::checkRequire", "TypeChecker"); LUAU_TIMETRACE_ARGUMENT("moduleInfo", moduleInfo.name.c_str()); if (moduleInfo.name.empty()) { if (currentModule->mode == Mode::Strict) { reportError(TypeError{location, UnknownRequire{}}); return errorRecoveryType(anyType); } return anyType; } // Types of requires that transitively refer to current module have to be replaced with 'any' std::string humanReadableName = resolver->getHumanReadableModuleName(moduleInfo.name); for (const auto& [location, path] : requireCycles) { if (!path.empty() && path.front() == humanReadableName) return anyType; } ModulePtr module = resolver->getModule(moduleInfo.name); if (!module) { // There are two reasons why we might fail to find the module: // either the file does not exist or there's a cycle. If there's a cycle // we will already have reported the error. if (!resolver->moduleExists(moduleInfo.name) && !moduleInfo.optional) reportError(TypeError{location, UnknownRequire{humanReadableName}}); return errorRecoveryType(scope); } if (module->type != SourceCode::Module) { reportError(location, IllegalRequire{humanReadableName, "Module is not a ModuleScript. It cannot be required."}); return errorRecoveryType(scope); } TypePackId modulePack = module->getModuleScope()->returnType; if (get(modulePack)) return errorRecoveryType(scope); std::optional moduleType = first(modulePack); if (!moduleType) { reportError(location, IllegalRequire{humanReadableName, "Module does not return exactly 1 value. It cannot be required."}); return errorRecoveryType(scope); } return *moduleType; } void TypeChecker::tablify(TypeId type) { type = follow(type); if (auto f = get(type)) *asMutable(type) = TableTypeVar{TableState::Free, f->level}; } TypeId TypeChecker::anyIfNonstrict(TypeId ty) const { if (isNonstrictMode()) return anyType; else return ty; } bool TypeChecker::unify(TypeId subTy, TypeId superTy, const Location& location) { UnifierOptions options; return unify(subTy, superTy, location, options); } bool TypeChecker::unify(TypeId subTy, TypeId superTy, const Location& location, const UnifierOptions& options) { Unifier state = mkUnifier(location); state.tryUnify(subTy, superTy, options.isFunctionCall); state.log.commit(); reportErrors(state.errors); return state.errors.empty(); } bool TypeChecker::unify(TypePackId subTy, TypePackId superTy, const Location& location, CountMismatch::Context ctx) { Unifier state = mkUnifier(location); state.ctx = ctx; state.tryUnify(subTy, superTy); state.log.commit(); reportErrors(state.errors); return state.errors.empty(); } bool TypeChecker::unifyWithInstantiationIfNeeded(const ScopePtr& scope, TypeId subTy, TypeId superTy, const Location& location) { Unifier state = mkUnifier(location); unifyWithInstantiationIfNeeded(scope, subTy, superTy, state); state.log.commit(); reportErrors(state.errors); return state.errors.empty(); } void TypeChecker::unifyWithInstantiationIfNeeded(const ScopePtr& scope, TypeId subTy, TypeId superTy, Unifier& state) { if (!maybeGeneric(subTy)) // Quick check to see if we definitely can't instantiate state.tryUnify(subTy, superTy, /*isFunctionCall*/ false); else if (!maybeGeneric(superTy) && isGeneric(subTy)) { // Quick check to see if we definitely have to instantiate TypeId instantiated = instantiate(scope, subTy, state.location); state.tryUnify(instantiated, superTy, /*isFunctionCall*/ false); } else { // First try unifying with the original uninstantiated type // but if that fails, try the instantiated one. Unifier child = state.makeChildUnifier(); child.tryUnify(subTy, superTy, /*isFunctionCall*/ false); if (!child.errors.empty()) { TypeId instantiated = instantiate(scope, subTy, state.location, &child.log); if (subTy == instantiated) { // Instantiating the argument made no difference, so just report any child errors state.log.concat(std::move(child.log)); state.errors.insert(state.errors.end(), child.errors.begin(), child.errors.end()); } else { state.tryUnify(instantiated, superTy, /*isFunctionCall*/ false); } } else { state.log.concat(std::move(child.log)); } } } bool Anyification::isDirty(TypeId ty) { if (ty->persistent) return false; if (const TableTypeVar* ttv = log->getMutable(ty)) return (ttv->state == TableState::Free || ttv->state == TableState::Unsealed); else if (log->getMutable(ty)) return true; else if (get(ty)) return true; else return false; } bool Anyification::isDirty(TypePackId tp) { if (tp->persistent) return false; if (log->getMutable(tp)) return true; else return false; } TypeId Anyification::clean(TypeId ty) { LUAU_ASSERT(isDirty(ty)); if (const TableTypeVar* ttv = log->getMutable(ty)) { TableTypeVar clone = TableTypeVar{ttv->props, ttv->indexer, ttv->level, TableState::Sealed}; clone.definitionModuleName = ttv->definitionModuleName; clone.name = ttv->name; clone.syntheticName = ttv->syntheticName; clone.tags = ttv->tags; TypeId res = addType(std::move(clone)); asMutable(res)->normal = ty->normal; return res; } else if (auto ctv = get(ty)) { if (FFlag::LuauQuantifyConstrained) { std::vector copy = ctv->parts; for (TypeId& ty : copy) ty = replace(ty); TypeId res = copy.size() == 1 ? copy[0] : addType(UnionTypeVar{std::move(copy)}); auto [t, ok] = normalize(res, *arena, *iceHandler); if (!ok) normalizationTooComplex = true; return t; } else { auto [t, ok] = normalize(ty, *arena, *iceHandler); if (!ok) normalizationTooComplex = true; return t; } } else return anyType; } TypePackId Anyification::clean(TypePackId tp) { LUAU_ASSERT(isDirty(tp)); return anyTypePack; } TypeId TypeChecker::quantify(const ScopePtr& scope, TypeId ty, Location location) { ty = follow(ty); const FunctionTypeVar* ftv = get(ty); if (FFlag::LuauAlwaysQuantify) { if (ftv) Luau::quantify(ty, scope->level); } else { if (ftv && ftv->generics.empty() && ftv->genericPacks.empty()) Luau::quantify(ty, scope->level); } if (FFlag::LuauLowerBoundsCalculation && ftv) { auto [t, ok] = Luau::normalize(ty, currentModule, *iceHandler); if (!ok) reportError(location, NormalizationTooComplex{}); return t; } return ty; } TypeId TypeChecker::instantiate(const ScopePtr& scope, TypeId ty, Location location, const TxnLog* log) { ty = follow(ty); const FunctionTypeVar* ftv = get(ty); if (ftv && ftv->hasNoGenerics) return ty; Instantiation instantiation{log, ¤tModule->internalTypes, scope->level}; if (FFlag::LuauAutocompleteDynamicLimits && instantiationChildLimit) instantiation.childLimit = *instantiationChildLimit; std::optional instantiated = instantiation.substitute(ty); if (instantiated.has_value()) return *instantiated; else { reportError(location, UnificationTooComplex{}); return errorRecoveryType(scope); } } TypeId TypeChecker::anyify(const ScopePtr& scope, TypeId ty, Location location) { if (FFlag::LuauLowerBoundsCalculation) { auto [t, ok] = normalize(ty, currentModule, *iceHandler); if (!ok) reportError(location, NormalizationTooComplex{}); ty = t; } Anyification anyification{¤tModule->internalTypes, iceHandler, anyType, anyTypePack}; std::optional any = anyification.substitute(ty); if (anyification.normalizationTooComplex) reportError(location, NormalizationTooComplex{}); if (any.has_value()) return *any; else { reportError(location, UnificationTooComplex{}); return errorRecoveryType(anyType); } } TypePackId TypeChecker::anyify(const ScopePtr& scope, TypePackId ty, Location location) { if (FFlag::LuauLowerBoundsCalculation) { auto [t, ok] = normalize(ty, currentModule, *iceHandler); if (!ok) reportError(location, NormalizationTooComplex{}); ty = t; } Anyification anyification{¤tModule->internalTypes, iceHandler, anyType, anyTypePack}; std::optional any = anyification.substitute(ty); if (any.has_value()) return *any; else { reportError(location, UnificationTooComplex{}); return errorRecoveryTypePack(anyTypePack); } } void TypeChecker::reportError(const TypeError& error) { if (currentModule->mode == Mode::NoCheck) return; currentModule->errors.push_back(error); currentModule->errors.back().moduleName = currentModuleName; } void TypeChecker::reportError(const Location& location, TypeErrorData errorData) { return reportError(TypeError{location, std::move(errorData)}); } void TypeChecker::reportErrors(const ErrorVec& errors) { for (const auto& err : errors) reportError(err); } void TypeChecker::ice(const std::string& message, const Location& location) { iceHandler->ice(message, location); } void TypeChecker::ice(const std::string& message) { iceHandler->ice(message); } void TypeChecker::prepareErrorsForDisplay(ErrorVec& errVec) { // Remove errors with names that were generated by recovery from a parse error errVec.erase(std::remove_if(errVec.begin(), errVec.end(), [](auto& err) { return containsParseErrorName(err); }), errVec.end()); for (auto& err : errVec) { if (auto utk = get(err)) diagnoseMissingTableKey(utk, err.data); } } void TypeChecker::diagnoseMissingTableKey(UnknownProperty* utk, TypeErrorData& data) { std::string_view sv(utk->key); std::set candidates; auto accumulate = [&](const TableTypeVar::Props& props) { for (const auto& [name, ty] : props) { if (sv != name && equalsLower(sv, name)) candidates.insert(name); } }; if (auto ttv = getTableType(utk->table)) accumulate(ttv->props); else if (auto ctv = get(follow(utk->table))) { while (ctv) { accumulate(ctv->props); if (!ctv->parent) break; ctv = get(*ctv->parent); LUAU_ASSERT(ctv); } } if (!candidates.empty()) data = TypeErrorData(UnknownPropButFoundLikeProp{utk->table, utk->key, candidates}); } LUAU_NOINLINE void TypeChecker::reportErrorCodeTooComplex(const Location& location) { reportError(TypeError{location, CodeTooComplex{}}); } // Creates a new Scope but without carrying forward the varargs from the parent. ScopePtr TypeChecker::childFunctionScope(const ScopePtr& parent, const Location& location, int subLevel) { ScopePtr scope = std::make_shared(parent, subLevel); currentModule->scopes.push_back(std::make_pair(location, scope)); return scope; } // Creates a new Scope and carries forward the varargs from the parent. ScopePtr TypeChecker::childScope(const ScopePtr& parent, const Location& location) { ScopePtr scope = std::make_shared(parent); scope->level = parent->level; scope->varargPack = parent->varargPack; currentModule->scopes.push_back(std::make_pair(location, scope)); return scope; } void TypeChecker::merge(RefinementMap& l, const RefinementMap& r) { Luau::merge(l, r, [this](TypeId a, TypeId b) { // TODO: normalize(UnionTypeVar{{a, b}}) std::unordered_set set; if (auto utv = get(follow(a))) set.insert(begin(utv), end(utv)); else set.insert(a); if (auto utv = get(follow(b))) set.insert(begin(utv), end(utv)); else set.insert(b); std::vector options(set.begin(), set.end()); if (set.size() == 1) return options[0]; return addType(UnionTypeVar{std::move(options)}); }); } Unifier TypeChecker::mkUnifier(const Location& location) { return Unifier{¤tModule->internalTypes, currentModule->mode, location, Variance::Covariant, unifierState}; } TypeId TypeChecker::freshType(const ScopePtr& scope) { return freshType(scope->level); } TypeId TypeChecker::freshType(TypeLevel level) { return currentModule->internalTypes.addType(TypeVar(FreeTypeVar(level))); } TypeId TypeChecker::singletonType(bool value) { return value ? getSingletonTypes().trueType : getSingletonTypes().falseType; } TypeId TypeChecker::singletonType(std::string value) { // TODO: cache singleton types return currentModule->internalTypes.addType(TypeVar(SingletonTypeVar(StringSingleton{std::move(value)}))); } TypeId TypeChecker::errorRecoveryType(const ScopePtr& scope) { return getSingletonTypes().errorRecoveryType(); } TypeId TypeChecker::errorRecoveryType(TypeId guess) { return getSingletonTypes().errorRecoveryType(guess); } TypePackId TypeChecker::errorRecoveryTypePack(const ScopePtr& scope) { return getSingletonTypes().errorRecoveryTypePack(); } TypePackId TypeChecker::errorRecoveryTypePack(TypePackId guess) { return getSingletonTypes().errorRecoveryTypePack(guess); } TypeIdPredicate TypeChecker::mkTruthyPredicate(bool sense) { return [this, sense](TypeId ty) -> std::optional { // any/error/free gets a special pass unconditionally because they can't be decided. if (get(ty) || get(ty) || get(ty)) return ty; // maps boolean primitive to the corresponding singleton equal to sense if (isPrim(ty, PrimitiveTypeVar::Boolean)) return singletonType(sense); // if we have boolean singleton, eliminate it if the sense doesn't match with that singleton if (auto boolean = get(get(ty))) return boolean->value == sense ? std::optional(ty) : std::nullopt; // if we have nil, eliminate it if sense is true, otherwise take it if (isNil(ty)) return sense ? std::nullopt : std::optional(ty); // at this point, anything else is kept if sense is true, or replaced by nil if (FFlag::LuauFalsyPredicateReturnsNilInstead) return sense ? ty : nilType; else return sense ? std::optional(ty) : std::nullopt; }; } std::optional TypeChecker::filterMap(TypeId type, TypeIdPredicate predicate) { std::vector types = Luau::filterMap(type, predicate); if (!types.empty()) return types.size() == 1 ? types[0] : addType(UnionTypeVar{std::move(types)}); return std::nullopt; } std::optional TypeChecker::pickTypesFromSense(TypeId type, bool sense) { return filterMap(type, mkTruthyPredicate(sense)); } TypeId TypeChecker::addTV(TypeVar&& tv) { return currentModule->internalTypes.addType(std::move(tv)); } TypePackId TypeChecker::addTypePack(TypePackVar&& tv) { return currentModule->internalTypes.addTypePack(std::move(tv)); } TypePackId TypeChecker::addTypePack(TypePack&& tp) { return addTypePack(TypePackVar(std::move(tp))); } TypePackId TypeChecker::addTypePack(const std::vector& ty) { return addTypePack(ty, std::nullopt); } TypePackId TypeChecker::addTypePack(const std::vector& ty, std::optional tail) { return addTypePack(TypePackVar(TypePack{ty, tail})); } TypePackId TypeChecker::addTypePack(std::initializer_list&& ty) { return addTypePack(TypePackVar(TypePack{std::vector(begin(ty), end(ty)), std::nullopt})); } TypePackId TypeChecker::freshTypePack(const ScopePtr& scope) { return freshTypePack(scope->level); } TypePackId TypeChecker::freshTypePack(TypeLevel level) { return addTypePack(TypePackVar(FreeTypePack(level))); } TypeId TypeChecker::resolveType(const ScopePtr& scope, const AstType& annotation) { if (const auto& lit = annotation.as()) { std::optional tf; if (lit->prefix) tf = scope->lookupImportedType(lit->prefix->value, lit->name.value); else if (FFlag::DebugLuauMagicTypes && lit->name == "_luau_ice") ice("_luau_ice encountered", lit->location); else if (FFlag::DebugLuauMagicTypes && lit->name == "_luau_print") { if (lit->parameters.size != 1 || !lit->parameters.data[0].type) { reportError(TypeError{annotation.location, GenericError{"_luau_print requires one generic parameter"}}); return errorRecoveryType(anyType); } ToStringOptions opts; opts.exhaustive = true; opts.maxTableLength = 0; opts.useLineBreaks = true; TypeId param = resolveType(scope, *lit->parameters.data[0].type); luauPrintLine(format("_luau_print\t%s\t|\t%s", toString(param, opts).c_str(), toString(lit->location).c_str())); return param; } else tf = scope->lookupType(lit->name.value); if (!tf) { if (lit->name == kParseNameError) return errorRecoveryType(scope); std::string typeName; if (lit->prefix) typeName = std::string(lit->prefix->value) + "."; typeName += lit->name.value; if (scope->lookupPack(typeName)) reportError(TypeError{annotation.location, SwappedGenericTypeParameter{typeName, SwappedGenericTypeParameter::Type}}); else reportError(TypeError{annotation.location, UnknownSymbol{typeName, UnknownSymbol::Type}}); return errorRecoveryType(scope); } if (lit->parameters.size == 0 && tf->typeParams.empty() && tf->typePackParams.empty()) return tf->type; bool parameterCountErrorReported = false; bool hasDefaultTypes = std::any_of(tf->typeParams.begin(), tf->typeParams.end(), [](auto&& el) { return el.defaultValue.has_value(); }); bool hasDefaultPacks = std::any_of(tf->typePackParams.begin(), tf->typePackParams.end(), [](auto&& el) { return el.defaultValue.has_value(); }); if (!lit->hasParameterList) { if ((!tf->typeParams.empty() && !hasDefaultTypes) || (!tf->typePackParams.empty() && !hasDefaultPacks)) { reportError(TypeError{annotation.location, GenericError{"Type parameter list is required"}}); parameterCountErrorReported = true; } } std::vector typeParams; std::vector extraTypes; std::vector typePackParams; for (size_t i = 0; i < lit->parameters.size; ++i) { if (AstType* type = lit->parameters.data[i].type) { TypeId ty = resolveType(scope, *type); if (typeParams.size() < tf->typeParams.size() || tf->typePackParams.empty()) typeParams.push_back(ty); else if (typePackParams.empty()) extraTypes.push_back(ty); else reportError(TypeError{annotation.location, GenericError{"Type parameters must come before type pack parameters"}}); } else if (AstTypePack* typePack = lit->parameters.data[i].typePack) { TypePackId tp = resolveTypePack(scope, *typePack); // If we have collected an implicit type pack, materialize it if (typePackParams.empty() && !extraTypes.empty()) typePackParams.push_back(addTypePack(extraTypes)); // If we need more regular types, we can use single element type packs to fill those in if (typeParams.size() < tf->typeParams.size() && size(tp) == 1 && finite(tp) && first(tp)) typeParams.push_back(*first(tp)); else typePackParams.push_back(tp); } } // If we still haven't meterialized an implicit type pack, do it now if (typePackParams.empty() && !extraTypes.empty()) typePackParams.push_back(addTypePack(extraTypes)); size_t typesProvided = typeParams.size(); size_t typesRequired = tf->typeParams.size(); size_t packsProvided = typePackParams.size(); size_t packsRequired = tf->typePackParams.size(); bool notEnoughParameters = (typesProvided < typesRequired && packsProvided == 0) || (typesProvided == typesRequired && packsProvided < packsRequired); bool hasDefaultParameters = hasDefaultTypes || hasDefaultPacks; // Add default type and type pack parameters if that's required and it's possible if (notEnoughParameters && hasDefaultParameters) { // 'applyTypeFunction' is used to substitute default types that reference previous generic types ApplyTypeFunction applyTypeFunction{¤tModule->internalTypes, scope->level}; for (size_t i = 0; i < typesProvided; ++i) applyTypeFunction.typeArguments[tf->typeParams[i].ty] = typeParams[i]; if (typesProvided < typesRequired) { for (size_t i = typesProvided; i < typesRequired; ++i) { TypeId defaultTy = tf->typeParams[i].defaultValue.value_or(nullptr); if (!defaultTy) break; std::optional maybeInstantiated = applyTypeFunction.substitute(defaultTy); if (!maybeInstantiated.has_value()) { reportError(annotation.location, UnificationTooComplex{}); maybeInstantiated = errorRecoveryType(scope); } applyTypeFunction.typeArguments[tf->typeParams[i].ty] = *maybeInstantiated; typeParams.push_back(*maybeInstantiated); } } for (size_t i = 0; i < packsProvided; ++i) applyTypeFunction.typePackArguments[tf->typePackParams[i].tp] = typePackParams[i]; if (packsProvided < packsRequired) { for (size_t i = packsProvided; i < packsRequired; ++i) { TypePackId defaultTp = tf->typePackParams[i].defaultValue.value_or(nullptr); if (!defaultTp) break; std::optional maybeInstantiated = applyTypeFunction.substitute(defaultTp); if (!maybeInstantiated.has_value()) { reportError(annotation.location, UnificationTooComplex{}); maybeInstantiated = errorRecoveryTypePack(scope); } applyTypeFunction.typePackArguments[tf->typePackParams[i].tp] = *maybeInstantiated; typePackParams.push_back(*maybeInstantiated); } } } // If we didn't combine regular types into a type pack and we're still one type pack short, provide an empty type pack if (extraTypes.empty() && typePackParams.size() + 1 == tf->typePackParams.size()) typePackParams.push_back(addTypePack({})); if (typeParams.size() != tf->typeParams.size() || typePackParams.size() != tf->typePackParams.size()) { if (!parameterCountErrorReported) reportError( TypeError{annotation.location, IncorrectGenericParameterCount{lit->name.value, *tf, typeParams.size(), typePackParams.size()}}); // Pad the types out with error recovery types while (typeParams.size() < tf->typeParams.size()) typeParams.push_back(errorRecoveryType(scope)); while (typePackParams.size() < tf->typePackParams.size()) typePackParams.push_back(errorRecoveryTypePack(scope)); } bool sameTys = std::equal(typeParams.begin(), typeParams.end(), tf->typeParams.begin(), tf->typeParams.end(), [](auto&& itp, auto&& tp) { return itp == tp.ty; }); bool sameTps = std::equal( typePackParams.begin(), typePackParams.end(), tf->typePackParams.begin(), tf->typePackParams.end(), [](auto&& itpp, auto&& tpp) { return itpp == tpp.tp; }); // If the generic parameters and the type arguments are the same, we are about to // perform an identity substitution, which we can just short-circuit. if (sameTys && sameTps) return tf->type; return instantiateTypeFun(scope, *tf, typeParams, typePackParams, annotation.location); } else if (const auto& table = annotation.as()) { TableTypeVar::Props props; std::optional tableIndexer; for (const auto& prop : table->props) props[prop.name.value] = {resolveType(scope, *prop.type)}; if (const auto& indexer = table->indexer) tableIndexer = TableIndexer(resolveType(scope, *indexer->indexType), resolveType(scope, *indexer->resultType)); TableTypeVar ttv{props, tableIndexer, scope->level, TableState::Sealed}; ttv.definitionModuleName = currentModuleName; return addType(std::move(ttv)); } else if (const auto& func = annotation.as()) { ScopePtr funcScope = childScope(scope, func->location); funcScope->level = scope->level.incr(); auto [generics, genericPacks] = createGenericTypes(funcScope, std::nullopt, annotation, func->generics, func->genericPacks); TypePackId argTypes = resolveTypePack(funcScope, func->argTypes); TypePackId retTypes = resolveTypePack(funcScope, func->returnTypes); std::vector genericTys; genericTys.reserve(generics.size()); std::transform(generics.begin(), generics.end(), std::back_inserter(genericTys), [](auto&& el) { return el.ty; }); std::vector genericTps; genericTps.reserve(genericPacks.size()); std::transform(genericPacks.begin(), genericPacks.end(), std::back_inserter(genericTps), [](auto&& el) { return el.tp; }); TypeId fnType = addType(FunctionTypeVar{funcScope->level, std::move(genericTys), std::move(genericTps), argTypes, retTypes}); FunctionTypeVar* ftv = getMutable(fnType); ftv->argNames.reserve(func->argNames.size); for (const auto& el : func->argNames) { if (el) ftv->argNames.push_back(FunctionArgument{el->first.value, el->second}); else ftv->argNames.push_back(std::nullopt); } return fnType; } else if (auto typeOf = annotation.as()) { TypeId ty = checkExpr(scope, *typeOf->expr).type; return ty; } else if (const auto& un = annotation.as()) { std::vector types; for (AstType* ann : un->types) types.push_back(resolveType(scope, *ann)); return addType(UnionTypeVar{types}); } else if (const auto& un = annotation.as()) { std::vector types; for (AstType* ann : un->types) types.push_back(resolveType(scope, *ann)); return addType(IntersectionTypeVar{types}); } else if (const auto& tsb = annotation.as()) { return singletonType(tsb->value); } else if (const auto& tss = annotation.as()) { return singletonType(std::string(tss->value.data, tss->value.size)); } else if (annotation.is()) return errorRecoveryType(scope); else { reportError(TypeError{annotation.location, GenericError{"Unknown type annotation?"}}); return errorRecoveryType(scope); } } TypePackId TypeChecker::resolveTypePack(const ScopePtr& scope, const AstTypeList& types) { if (types.types.size == 0 && types.tailType) { return resolveTypePack(scope, *types.tailType); } else if (types.types.size > 0) { std::vector head; for (AstType* ann : types.types) head.push_back(resolveType(scope, *ann)); std::optional tail = types.tailType ? std::optional(resolveTypePack(scope, *types.tailType)) : std::nullopt; return addTypePack(TypePack{head, tail}); } return addTypePack(TypePack{}); } TypePackId TypeChecker::resolveTypePack(const ScopePtr& scope, const AstTypePack& annotation) { if (const AstTypePackVariadic* variadic = annotation.as()) { return addTypePack(TypePackVar{VariadicTypePack{resolveType(scope, *variadic->variadicType)}}); } else if (const AstTypePackGeneric* generic = annotation.as()) { Name genericName = Name(generic->genericName.value); std::optional genericTy = scope->lookupPack(genericName); if (!genericTy) { if (scope->lookupType(genericName)) reportError(TypeError{generic->location, SwappedGenericTypeParameter{genericName, SwappedGenericTypeParameter::Pack}}); else reportError(TypeError{generic->location, UnknownSymbol{genericName, UnknownSymbol::Type}}); return errorRecoveryTypePack(scope); } return *genericTy; } else if (const AstTypePackExplicit* explicitTp = annotation.as()) { std::vector types; for (auto type : explicitTp->typeList.types) types.push_back(resolveType(scope, *type)); if (auto tailType = explicitTp->typeList.tailType) return addTypePack(types, resolveTypePack(scope, *tailType)); return addTypePack(types); } else { ice("Unknown AstTypePack kind"); } } bool ApplyTypeFunction::isDirty(TypeId ty) { if (FFlag::LuauApplyTypeFunctionFix && typeArguments.count(ty)) return true; else if (!FFlag::LuauApplyTypeFunctionFix && get(ty)) return true; else if (const FreeTypeVar* ftv = get(ty)) { if (ftv->forwardedTypeAlias) encounteredForwardedType = true; return false; } else return false; } bool ApplyTypeFunction::isDirty(TypePackId tp) { if (FFlag::LuauApplyTypeFunctionFix && typePackArguments.count(tp)) return true; else if (!FFlag::LuauApplyTypeFunctionFix && get(tp)) return true; else return false; } bool ApplyTypeFunction::ignoreChildren(TypeId ty) { if (get(ty)) return true; else return false; } bool ApplyTypeFunction::ignoreChildren(TypePackId tp) { if (get(tp)) return true; else return false; } TypeId ApplyTypeFunction::clean(TypeId ty) { TypeId& arg = typeArguments[ty]; if (FFlag::LuauApplyTypeFunctionFix) { LUAU_ASSERT(arg); return arg; } else if (arg) return arg; else return addType(FreeTypeVar{level}); } TypePackId ApplyTypeFunction::clean(TypePackId tp) { TypePackId& arg = typePackArguments[tp]; if (FFlag::LuauApplyTypeFunctionFix) { LUAU_ASSERT(arg); return arg; } else if (arg) return arg; else return addTypePack(FreeTypePack{level}); } TypeId TypeChecker::instantiateTypeFun(const ScopePtr& scope, const TypeFun& tf, const std::vector& typeParams, const std::vector& typePackParams, const Location& location) { if (tf.typeParams.empty() && tf.typePackParams.empty()) return tf.type; ApplyTypeFunction applyTypeFunction{¤tModule->internalTypes, scope->level}; for (size_t i = 0; i < tf.typeParams.size(); ++i) applyTypeFunction.typeArguments[tf.typeParams[i].ty] = typeParams[i]; for (size_t i = 0; i < tf.typePackParams.size(); ++i) applyTypeFunction.typePackArguments[tf.typePackParams[i].tp] = typePackParams[i]; std::optional maybeInstantiated = applyTypeFunction.substitute(tf.type); if (!maybeInstantiated.has_value()) { reportError(location, UnificationTooComplex{}); return errorRecoveryType(scope); } if (applyTypeFunction.encounteredForwardedType) { reportError(TypeError{location, GenericError{"Recursive type being used with different parameters"}}); return errorRecoveryType(scope); } TypeId instantiated = *maybeInstantiated; TypeId target = follow(instantiated); bool needsClone = follow(tf.type) == target; bool shouldMutate = (!FFlag::LuauOnlyMutateInstantiatedTables || getTableType(tf.type)); TableTypeVar* ttv = getMutableTableType(target); if (shouldMutate && ttv && needsClone) { // Substitution::clone is a shallow clone. If this is a metatable type, we // want to mutate its table, so we need to explicitly clone that table as // well. If we don't, we will mutate another module's type surface and cause // a use-after-free. if (get(target)) { instantiated = applyTypeFunction.clone(tf.type); MetatableTypeVar* mtv = getMutable(instantiated); mtv->table = applyTypeFunction.clone(mtv->table); ttv = getMutable(mtv->table); } if (get(target)) { instantiated = applyTypeFunction.clone(tf.type); ttv = getMutable(instantiated); } } if (shouldMutate && ttv) { ttv->instantiatedTypeParams = typeParams; ttv->instantiatedTypePackParams = typePackParams; ttv->definitionModuleName = currentModuleName; } return instantiated; } GenericTypeDefinitions TypeChecker::createGenericTypes(const ScopePtr& scope, std::optional levelOpt, const AstNode& node, const AstArray& genericNames, const AstArray& genericPackNames, bool useCache) { LUAU_ASSERT(scope->parent); const TypeLevel level = levelOpt.value_or(scope->level); std::vector generics; for (const AstGenericType& generic : genericNames) { std::optional defaultValue; if (generic.defaultValue) defaultValue = resolveType(scope, *generic.defaultValue); Name n = generic.name.value; // These generics are the only thing that will ever be added to scope, so we can be certain that // a collision can only occur when two generic typevars have the same name. if (scope->privateTypeBindings.count(n) || scope->privateTypePackBindings.count(n)) { // TODO(jhuelsman): report the exact span of the generic type parameter whose name is a duplicate. reportError(TypeError{node.location, DuplicateGenericParameter{n}}); } TypeId g; if (useCache) { TypeId& cached = scope->parent->typeAliasTypeParameters[n]; if (!cached) cached = addType(GenericTypeVar{level, n}); g = cached; } else { g = addType(Unifiable::Generic{level, n}); } generics.push_back({g, defaultValue}); scope->privateTypeBindings[n] = TypeFun{{}, g}; } std::vector genericPacks; for (const AstGenericTypePack& genericPack : genericPackNames) { std::optional defaultValue; if (genericPack.defaultValue) defaultValue = resolveTypePack(scope, *genericPack.defaultValue); Name n = genericPack.name.value; // These generics are the only thing that will ever be added to scope, so we can be certain that // a collision can only occur when two generic typevars have the same name. if (scope->privateTypePackBindings.count(n) || scope->privateTypeBindings.count(n)) { // TODO(jhuelsman): report the exact span of the generic type parameter whose name is a duplicate. reportError(TypeError{node.location, DuplicateGenericParameter{n}}); } TypePackId& cached = scope->parent->typeAliasTypePackParameters[n]; if (!cached) cached = addTypePack(TypePackVar{Unifiable::Generic{level, n}}); genericPacks.push_back({cached, defaultValue}); scope->privateTypePackBindings[n] = cached; } return {generics, genericPacks}; } void TypeChecker::refineLValue(const LValue& lvalue, RefinementMap& refis, const ScopePtr& scope, TypeIdPredicate predicate) { const LValue* target = &lvalue; std::optional key; // If set, we know we took the base of the lvalue path and should be walking down each option of the base's type. auto ty = resolveLValue(scope, *target); if (!ty) return; // Do nothing. An error was already reported. // If the provided lvalue is a local or global, then that's without a doubt the target. // However, if there is a base lvalue, then we'll want that to be the target iff the base is a union type. if (auto base = baseof(lvalue)) { std::optional baseTy = resolveLValue(scope, *base); if (baseTy && get(follow(*baseTy))) { ty = baseTy; target = base; key = lvalue; } } // If we do not have a key, it means we're not trying to discriminate anything, so it's a simple matter of just filtering for a subset. if (!key) { if (std::optional result = filterMap(*ty, predicate)) addRefinement(refis, *target, *result); else addRefinement(refis, *target, errorRecoveryType(scope)); return; } // Otherwise, we'll want to walk each option of ty, get its index type, and filter that. auto utv = get(follow(*ty)); LUAU_ASSERT(utv); std::unordered_set viableTargetOptions; std::unordered_set viableChildOptions; // There may be additional refinements that apply. We add those here too. for (TypeId option : utv) { std::optional discriminantTy; if (auto field = Luau::get(*key)) // need to fully qualify Luau::get because of ADL. discriminantTy = getIndexTypeFromType(scope, option, field->key, Location(), false); else LUAU_ASSERT(!"Unhandled LValue alternative?"); if (!discriminantTy) return; // Do nothing. An error was already reported, as per usual. if (std::optional result = filterMap(*discriminantTy, predicate)) { viableTargetOptions.insert(option); viableChildOptions.insert(*result); } } auto intoType = [this](const std::unordered_set& s) -> std::optional { if (s.empty()) return std::nullopt; // TODO: allocate UnionTypeVar and just normalize. std::vector options(s.begin(), s.end()); if (options.size() == 1) return options[0]; return addType(UnionTypeVar{std::move(options)}); }; if (std::optional viableTargetType = intoType(viableTargetOptions)) addRefinement(refis, *target, *viableTargetType); if (std::optional viableChildType = intoType(viableChildOptions)) addRefinement(refis, lvalue, *viableChildType); } std::optional TypeChecker::resolveLValue(const ScopePtr& scope, const LValue& lvalue) { // We want to be walking the Scope parents. // We'll also want to walk up the LValue path. As we do this, we need to save each LValue because we must walk back. // For example: // There exists an entry t.x. // We are asked to look for t.x.y. // We need to search in the provided Scope. Find t.x.y first. // We fail to find t.x.y. Try t.x. We found it. Now we must return the type of the property y from the mapped-to type of t.x. // If we completely fail to find the Symbol t but the Scope has that entry, then we should walk that all the way through and terminate. const Symbol symbol = getBaseSymbol(lvalue); ScopePtr currentScope = scope; while (currentScope) { std::optional found; const LValue* topLValue = nullptr; for (topLValue = &lvalue; topLValue; topLValue = baseof(*topLValue)) { if (auto it = currentScope->refinements.find(*topLValue); it != currentScope->refinements.end()) { found = it->second; break; } } if (!found) { // Should not be using scope->lookup. This is already recursive. if (auto it = currentScope->bindings.find(symbol); it != currentScope->bindings.end()) found = it->second.typeId; else { // Nothing exists in this Scope. Just skip and try the parent one. currentScope = currentScope->parent; continue; } } // We need to walk the l-value path in reverse, so we collect components into a vector std::vector childKeys; for (const LValue* curr = &lvalue; curr != topLValue; curr = baseof(*curr)) childKeys.push_back(curr); for (auto it = childKeys.rbegin(); it != childKeys.rend(); ++it) { const LValue& key = **it; // Symbol can happen. Skip. if (get(key)) continue; else if (auto field = get(key)) { found = getIndexTypeFromType(scope, *found, field->key, Location(), false); if (!found) return std::nullopt; // Turns out this type doesn't have the property at all. We're done. } else LUAU_ASSERT(!"New LValue alternative not handled here."); } return found; } // No entry for it at all. Can happen when LValue root is a global. return std::nullopt; } std::optional TypeChecker::resolveLValue(const RefinementMap& refis, const ScopePtr& scope, const LValue& lvalue) { if (auto it = refis.find(lvalue); it != refis.end()) return it->second; else return resolveLValue(scope, lvalue); } // Only should be used for refinements! // This can probably go away once we have something that can limit a free type's type domain. static bool isUndecidable(TypeId ty) { ty = follow(ty); return get(ty) || get(ty) || get(ty); } void TypeChecker::resolve(const PredicateVec& predicates, const ScopePtr& scope, bool sense) { resolve(predicates, scope->refinements, scope, sense); } void TypeChecker::resolve(const PredicateVec& predicates, RefinementMap& refis, const ScopePtr& scope, bool sense, bool fromOr) { for (const Predicate& c : predicates) resolve(c, refis, scope, sense, fromOr); } void TypeChecker::resolve(const Predicate& predicate, RefinementMap& refis, const ScopePtr& scope, bool sense, bool fromOr) { if (auto truthyP = get(predicate)) resolve(*truthyP, refis, scope, sense, fromOr); else if (auto andP = get(predicate)) resolve(*andP, refis, scope, sense); else if (auto orP = get(predicate)) resolve(*orP, refis, scope, sense); else if (auto notP = get(predicate)) resolve(notP->predicates, refis, scope, !sense, fromOr); else if (auto isaP = get(predicate)) resolve(*isaP, refis, scope, sense); else if (auto typeguardP = get(predicate)) resolve(*typeguardP, refis, scope, sense); else if (auto eqP = get(predicate)) resolve(*eqP, refis, scope, sense); else ice("Unhandled predicate kind"); } void TypeChecker::resolve(const TruthyPredicate& truthyP, RefinementMap& refis, const ScopePtr& scope, bool sense, bool fromOr) { std::optional ty = resolveLValue(refis, scope, truthyP.lvalue); if (ty && fromOr) return addRefinement(refis, truthyP.lvalue, *ty); refineLValue(truthyP.lvalue, refis, scope, mkTruthyPredicate(sense)); } void TypeChecker::resolve(const AndPredicate& andP, RefinementMap& refis, const ScopePtr& scope, bool sense) { if (!sense) { OrPredicate orP{ {NotPredicate{std::move(andP.lhs)}}, {NotPredicate{std::move(andP.rhs)}}, }; return resolve(orP, refis, scope, !sense); } resolve(andP.lhs, refis, scope, sense); resolve(andP.rhs, refis, scope, sense); } void TypeChecker::resolve(const OrPredicate& orP, RefinementMap& refis, const ScopePtr& scope, bool sense) { if (!sense) { AndPredicate andP{ {NotPredicate{std::move(orP.lhs)}}, {NotPredicate{std::move(orP.rhs)}}, }; return resolve(andP, refis, scope, !sense); } RefinementMap leftRefis; resolve(orP.lhs, leftRefis, scope, sense); RefinementMap rightRefis; resolve(orP.lhs, rightRefis, scope, !sense); resolve(orP.rhs, rightRefis, scope, sense, true); // :( merge(refis, leftRefis); merge(refis, rightRefis); } void TypeChecker::resolve(const IsAPredicate& isaP, RefinementMap& refis, const ScopePtr& scope, bool sense) { auto predicate = [&](TypeId option) -> std::optional { // This by itself is not truly enough to determine that A is stronger than B or vice versa. bool optionIsSubtype = canUnify(option, isaP.ty, isaP.location).empty(); bool targetIsSubtype = canUnify(isaP.ty, option, isaP.location).empty(); // If A is a superset of B, then if sense is true, we promote A to B, otherwise we keep A. if (!optionIsSubtype && targetIsSubtype) return sense ? isaP.ty : option; // If A is a subset of B, then if sense is true we pick A, otherwise we eliminate A. if (optionIsSubtype && !targetIsSubtype) return sense ? std::optional(option) : std::nullopt; // If neither has any relationship, we only return A if sense is false. if (!optionIsSubtype && !targetIsSubtype) return sense ? std::nullopt : std::optional(option); // If both are subtypes, then we're in one of the two situations: // 1. Instance₁ <: Instance₂ ∧ Instance₂ <: Instance₁ // 2. any <: Instance ∧ Instance <: any // Right now, we have to look at the types to see if they were undecidables. // By this point, we also know free tables are also subtypes and supertypes. if (optionIsSubtype && targetIsSubtype) { // We can only have (any, Instance) because the rhs is never undecidable right now. // So we can just return the right hand side immediately. // typeof(x) == "Instance" where x : any auto ttv = get(option); if (isUndecidable(option) || (ttv && ttv->state == TableState::Free)) return sense ? isaP.ty : option; // typeof(x) == "Instance" where x : Instance if (sense) return isaP.ty; } // local variable works around an odd gcc 9.3 warning: may be used uninitialized std::optional res = std::nullopt; return res; }; refineLValue(isaP.lvalue, refis, scope, predicate); } void TypeChecker::resolve(const TypeGuardPredicate& typeguardP, RefinementMap& refis, const ScopePtr& scope, bool sense) { // Rewrite the predicate 'type(foo) == "vector"' to be 'typeof(foo) == "Vector3"'. They're exactly identical. // This allows us to avoid writing in edge cases. if (!typeguardP.isTypeof && typeguardP.kind == "vector") return resolve(TypeGuardPredicate{std::move(typeguardP.lvalue), typeguardP.location, "Vector3", true}, refis, scope, sense); std::optional ty = resolveLValue(refis, scope, typeguardP.lvalue); if (!ty) return; // In certain cases, the value may actually be nil, but Luau doesn't know about it. So we whitelist this. if (sense && typeguardP.kind == "nil") { addRefinement(refis, typeguardP.lvalue, nilType); return; } using ConditionFunc = bool(TypeId); using SenseToTypeIdPredicate = std::function; auto mkFilter = [](ConditionFunc f, std::optional other = std::nullopt) -> SenseToTypeIdPredicate { return [f, other](bool sense) -> TypeIdPredicate { return [f, other, sense](TypeId ty) -> std::optional { if (f(ty) == sense) return ty; if (isUndecidable(ty)) return other.value_or(ty); return std::nullopt; }; }; }; // Note: "vector" never happens here at this point, so we don't have to write something for it. // clang-format off static const std::unordered_map primitives{ // Trivial primitives. {"nil", mkFilter(isNil, nilType)}, // This can still happen when sense is false! {"string", mkFilter(isString, stringType)}, {"number", mkFilter(isNumber, numberType)}, {"boolean", mkFilter(isBoolean, booleanType)}, {"thread", mkFilter(isThread, threadType)}, // Non-trivial primitives. {"table", mkFilter([](TypeId ty) -> bool { return isTableIntersection(ty) || get(ty) || get(ty); })}, {"function", mkFilter([](TypeId ty) -> bool { return isOverloadedFunction(ty) || get(ty); })}, // For now, we don't really care about being accurate with userdata if the typeguard was using typeof. {"userdata", mkFilter([](TypeId ty) -> bool { return get(ty); })}, }; // clang-format on if (auto it = primitives.find(typeguardP.kind); it != primitives.end()) { refineLValue(typeguardP.lvalue, refis, scope, it->second(sense)); return; } if (!typeguardP.isTypeof) return addRefinement(refis, typeguardP.lvalue, errorRecoveryType(scope)); auto typeFun = globalScope->lookupType(typeguardP.kind); if (!typeFun || !typeFun->typeParams.empty() || !typeFun->typePackParams.empty()) return addRefinement(refis, typeguardP.lvalue, errorRecoveryType(scope)); TypeId type = follow(typeFun->type); // We're only interested in the root class of any classes. if (auto ctv = get(type); !ctv || ctv->parent) return addRefinement(refis, typeguardP.lvalue, errorRecoveryType(scope)); // This probably hints at breaking out type filtering functions from the predicate solver so that typeof is not tightly coupled with IsA. // Until then, we rewrite this to be the same as using IsA. return resolve(IsAPredicate{std::move(typeguardP.lvalue), typeguardP.location, type}, refis, scope, sense); } void TypeChecker::resolve(const EqPredicate& eqP, RefinementMap& refis, const ScopePtr& scope, bool sense) { // This refinement will require success typing to do everything correctly. For now, we can get most of the way there. auto options = [](TypeId ty) -> std::vector { if (auto utv = get(follow(ty))) return std::vector(begin(utv), end(utv)); return {ty}; }; std::vector rhs = options(eqP.type); if (sense && std::any_of(rhs.begin(), rhs.end(), isUndecidable)) return; // Optimization: the other side has unknown types, so there's probably an overlap. Refining is no-op here. auto predicate = [&](TypeId option) -> std::optional { if (!sense && isNil(eqP.type)) return (isUndecidable(option) || !isNil(option)) ? std::optional(option) : std::nullopt; if (maybeSingleton(eqP.type)) { // Normally we'd write option <: eqP.type, but singletons are always the subtype, so we flip this. if (!sense || canUnify(eqP.type, option, eqP.location).empty()) return sense ? eqP.type : option; // local variable works around an odd gcc 9.3 warning: may be used uninitialized std::optional res = std::nullopt; return res; } return option; }; refineLValue(eqP.lvalue, refis, scope, predicate); } bool TypeChecker::isNonstrictMode() const { return (currentModule->mode == Mode::Nonstrict) || (currentModule->mode == Mode::NoCheck); } bool TypeChecker::useConstrainedIntersections() const { return FFlag::LuauLowerBoundsCalculation && !isNonstrictMode(); } std::vector TypeChecker::unTypePack(const ScopePtr& scope, TypePackId tp, size_t expectedLength, const Location& location) { TypePackId expectedTypePack = addTypePack({}); TypePack* expectedPack = getMutable(expectedTypePack); LUAU_ASSERT(expectedPack); for (size_t i = 0; i < expectedLength; ++i) expectedPack->head.push_back(freshType(scope)); unify(tp, expectedTypePack, location); for (TypeId& tp : expectedPack->head) tp = follow(tp); return expectedPack->head; } std::vector> TypeChecker::getScopes() const { return currentModule->scopes; } } // namespace Luau