// 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/Common.h" #include "Luau/ModuleResolver.h" #include "Luau/Quantify.h" #include "Luau/RecursionCounter.h" #include "Luau/Scope.h" #include "Luau/Substitution.h" #include "Luau/TopoSortStatements.h" #include "Luau/TypePack.h" #include "Luau/TypeUtils.h" #include "Luau/ToString.h" #include "Luau/TypeVar.h" #include "Luau/TimeTrace.h" #include #include LUAU_FASTFLAGVARIABLE(DebugLuauMagicTypes, false) LUAU_FASTINTVARIABLE(LuauTypeInferRecursionLimit, 500) LUAU_FASTINTVARIABLE(LuauTypeInferTypePackLoopLimit, 5000) LUAU_FASTINTVARIABLE(LuauCheckRecursionLimit, 500) LUAU_FASTFLAG(LuauKnowsTheDataModel3) LUAU_FASTFLAGVARIABLE(LuauEqConstraint, false) LUAU_FASTFLAGVARIABLE(LuauWeakEqConstraint, false) // Eventually removed as false. LUAU_FASTFLAG(LuauUseCommittingTxnLog) LUAU_FASTFLAGVARIABLE(DebugLuauFreezeDuringUnification, false) LUAU_FASTFLAGVARIABLE(LuauRecursiveTypeParameterRestriction, false) LUAU_FASTFLAGVARIABLE(LuauGenericFunctionsDontCacheTypeParams, false) LUAU_FASTFLAGVARIABLE(LuauImmutableTypes, false) LUAU_FASTFLAGVARIABLE(LuauQuantifyInPlace2, false) LUAU_FASTFLAGVARIABLE(LuauSealExports, false) LUAU_FASTFLAGVARIABLE(LuauSingletonTypes, false) LUAU_FASTFLAGVARIABLE(LuauDiscriminableUnions2, false) LUAU_FASTFLAGVARIABLE(LuauTypeAliasDefaults, false) LUAU_FASTFLAGVARIABLE(LuauExpectedTypesOfProperties, false) LUAU_FASTFLAGVARIABLE(LuauErrorRecoveryType, false) LUAU_FASTFLAGVARIABLE(LuauPropertiesGetExpectedType, false) LUAU_FASTFLAGVARIABLE(LuauAscribeCorrectLevelToInferredProperitesOfFreeTables, false) LUAU_FASTFLAGVARIABLE(LuauUnsealedTableLiteral, false) LUAU_FASTFLAGVARIABLE(LuauTwoPassAliasDefinitionFix, false) LUAU_FASTFLAGVARIABLE(LuauAssertStripsFalsyTypes, false) LUAU_FASTFLAGVARIABLE(LuauReturnAnyInsteadOfICE, false) // Eventually removed as false. LUAU_FASTFLAGVARIABLE(LuauAnotherTypeLevelFix, false) LUAU_FASTFLAG(LuauWidenIfSupertypeIsFree) LUAU_FASTFLAGVARIABLE(LuauDoNotTryToReduce, false) namespace Luau { 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; } } // 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) , optionalNumberType(getSingletonTypes().optionalNumberType) , 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) { LUAU_TIMETRACE_SCOPE("TypeChecker::check", "TypeChecker"); LUAU_TIMETRACE_ARGUMENT("module", module.name.c_str()); currentModule.reset(new Module()); currentModule->type = module.type; iceHandler->moduleName = module.name; 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()); checkBlock(moduleScope, *module.root); 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); bool encounteredFreeType = currentModule->clonePublicInterface(); if (encounteredFreeType) { reportError(TypeError{module.root->location, GenericError{"Free types leaked into this module's public interface. This is an internal Luau error; please report it."}}); } // 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.skipCacheForType.clear(); if (FFlag::LuauTwoPassAliasDefinitionFix) 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"); } // 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; } 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 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 (FFlag::LuauQuantifyInPlace2 ? containsFunctionCallOrReturn(**protoIter) : containsFunctionCall(**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()) { auto pair = checkFunctionSignature(scope, subLevel, *fun->func, fun->name->location, std::nullopt); auto [funTy, funScope] = pair; functionDecls[*protoIter] = pair; ++subLevel; TypeId leftType = 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 (FFlag::LuauTwoPassAliasDefinitionFix && 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))) { *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) { ExprResult result = checkExpr(scope, *statement.condition); ScopePtr ifScope = childScope(scope, statement.thenbody->location); reportErrors(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) { ExprResult result = checkExpr(scope, *statement.condition); ScopePtr whileScope = childScope(scope, statement.body->location); reportErrors(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::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); } } TypePackId retPack = checkExprList(scope, return_.location, return_.list, false, {}, expectedTypes).type; // 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::LuauUseCommittingTxnLog && FFlag::DebugLuauFreezeDuringUnification) freeze(currentModule->internalTypes); state.tryUnify(subTy, superTy); if (FFlag::LuauUseCommittingTxnLog && FFlag::DebugLuauFreezeDuringUnification) unfreeze(currentModule->internalTypes); if (!state.errors.empty() && !FFlag::LuauUseCommittingTxnLog) state.DEPRECATED_log.rollback(); if (state.errors.empty() && FFlag::LuauUseCommittingTxnLog) 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); if (FFlag::LuauUseCommittingTxnLog) 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 = follow(instantiate(scope, checkExpr(scope, *firstValue).type, firstValue->location)); } 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(TypeError{firstValue->location, TypeMismatch{globalScope->bindings[AstName{"next"}].typeId, 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*/ {}); if (FFlag::LuauUseCommittingTxnLog) 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->retType, 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) 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 (function.func->self) { AstExprIndexName* indexName = function.name->as(); if (!indexName) ice("member function declaration has malformed name expression"); TypeId selfTy = checkExpr(scope, *indexName->expr).type; TableTypeVar* tableSelf = getMutableTableType(selfTy); if (!tableSelf) { if (isTableIntersection(selfTy)) reportError(TypeError{function.location, CannotExtendTable{selfTy, CannotExtendTable::Property, indexName->index.value}}); else if (!get(selfTy) && !get(selfTy)) reportError(TypeError{function.location, OnlyTablesCanHaveMethods{selfTy}}); } else if (tableSelf->state == TableState::Sealed) reportError(TypeError{function.location, CannotExtendTable{selfTy, CannotExtendTable::Property, indexName->index.value}}); ty = follow(ty); if (tableSelf && tableSelf->state != TableState::Sealed) tableSelf->props[indexName->index.value] = {ty, /* deprecated */ false, {}, indexName->indexLocation}; 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 (tableSelf && tableSelf->state != TableState::Sealed) tableSelf->props[indexName->index.value] = { follow(quantify(funScope, ty, indexName->indexLocation)), /* deprecated */ false, {}, indexName->indexLocation}; } else { TypeId leftType = checkLValueBinding(scope, *function.name); checkFunctionBody(funScope, ty, *function.func); unify(ty, leftType, function.location); LUAU_ASSERT(function.name->is() || function.name->is()); if (auto exprIndexName = function.name->as()) { if (auto typeIt = currentModule->astTypes.find(exprIndexName->expr)) { if (auto ttv = getMutableTableType(*typeIt)) { if (auto it = ttv->props.find(exprIndexName->index.value); it != ttv->props.end()) it->second.type = follow(quantify(funScope, leftType, function.name->location)); } } } } } 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 (FFlag::LuauTwoPassAliasDefinitionFix && 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)}; if (FFlag::LuauTwoPassAliasDefinitionFix) 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}; } } else { // If the first pass failed (this should mean a duplicate definition), the second pass isn't going to be // interesting. if (FFlag::LuauTwoPassAliasDefinitionFix && 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 || (FFlag::LuauImmutableTypes && 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 ((FFlag::LuauImmutableTypes && !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.methodDefinitionLocations = ttv->methodDefinitionLocations; 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); ty = addType(std::move(clone)); } } 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 (!FFlag::LuauImmutableTypes || follow(ty)->owningArena == ¤tModule->internalTypes) mtv->syntheticName = name; } TypeId& bindingType = bindingsMap[name].type; bool ok = unify(ty, bindingType, typealias.location); if (FFlag::LuauTwoPassAliasDefinitionFix && ok) bindingType = ty; } } 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, {}, {})); 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 (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}; } ExprResult 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)}; } ExprResult 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 (FFlag::LuauSingletonTypes && (forceSingleton || (expectedType && maybeSingleton(*expectedType)))) result = {singletonType(bexpr->value)}; else result = {booleanType}; } else if (const AstExprConstantString* sexpr = expr.as()) { if (FFlag::LuauSingletonTypes && (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; } ExprResult 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)}; } ExprResult 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)}; } ExprResult 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)!"); } ExprResult TypeChecker::checkExpr(const ScopePtr& scope, const AstExprCall& expr) { ExprResult 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 (get(retPack)) { TypeId head = freshType(scope); TypePackId pack = addTypePack(TypePackVar{TypePack{{head}, freshTypePack(scope)}}); 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); } ExprResult 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 Name& name, const Location& location, bool addErrors) { type = follow(type); if (get(type) || get(type)) return type; tablify(type); if (FFlag::LuauDiscriminableUnions2) { if (isString(type)) { std::optional mtIndex = findMetatableEntry(stringType, "__index", location); LUAU_ASSERT(mtIndex); type = *mtIndex; } } else { const PrimitiveTypeVar* primitiveType = get(type); if (primitiveType && primitiveType->type == PrimitiveTypeVar::String) { if (std::optional mtIndex = findMetatableEntry(type, "__index", location)) type = *mtIndex; } } if (TableTypeVar* tableType = getMutableTableType(type)) { const auto& it = tableType->props.find(name); if (it != tableType->props.end()) return it->second.type; else if (auto indexer = tableType->indexer) { tryUnify(stringType, indexer->indexType, location); return indexer->indexResultType; } else if (tableType->state == TableState::Free) { TypeId result = FFlag::LuauAscribeCorrectLevelToInferredProperitesOfFreeTables ? freshType(tableType->level) : freshType(scope); tableType->props[name] = {result}; return result; } auto found = findTablePropertyRespectingMeta(type, name, location); if (found) 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); // 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; } 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); 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 (FFlag::LuauDoNotTryToReduce) { if (parts.size() == 1) return parts[0]; return addType(IntersectionTypeVar{std::move(parts)}); // Not at all correct. } else { // TODO(amccord): Write some logic to correctly handle intersections. CLI-34659 std::vector result = reduceUnion(parts); if (result.size() == 1) return result[0]; return addType(IntersectionTypeVar{result}); } } if (addErrors) reportError(location, UnknownProperty{type, name}); return std::nullopt; } std::vector TypeChecker::reduceUnion(const std::vector& types) { std::set s; for (TypeId t : types) { if (const UnionTypeVar* utv = get(follow(t))) { std::vector r = reduceUnion(utv->options); for (TypeId ty : r) s.insert(ty); } else s.insert(t); } // If any of them are ErrorTypeVars/AnyTypeVars, decay into them. for (TypeId t : s) { t = follow(t); if (get(t) || get(t)) return {t}; } std::vector r(s.begin(), s.end()); std::sort(r.begin(), r.end()); return r; } std::optional TypeChecker::tryStripUnionFromNil(TypeId ty) { if (const UnionTypeVar* utv = get(ty)) { bool hasNil = false; for (TypeId option : utv) { if (isNil(option)) { hasNil = true; break; } } if (!hasNil) 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) { if (isOptional(ty)) { if (std::optional strippedUnion = tryStripUnionFromNil(follow(ty))) { reportError(location, OptionalValueAccess{ty}); return follow(*strippedUnion); } } return ty; } ExprResult 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}; } ExprResult 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(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 (FFlag::LuauPropertiesGetExpectedType && 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 && isString(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); } ExprResult TypeChecker::checkExpr(const ScopePtr& scope, const AstExprTable& expr, std::optional expectedType) { RecursionLimiter _rl(&recursionCount, FInt::LuauTypeInferRecursionLimit); 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 (FFlag::LuauExpectedTypesOfProperties) 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 (FFlag::LuauExpectedTypesOfProperties && 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)}; } ExprResult TypeChecker::checkExpr(const ScopePtr& scope, const AstExprUnary& expr) { ExprResult 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); if (FFlag::LuauUseCommittingTxnLog) 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); reportErrors(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); if (FFlag::LuauUseCommittingTxnLog) 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)); // TODO: this check seems odd, the second part is redundant // is it meant to be if (leftMetatable && rightMetatable && leftMetatable != rightMetatable) if (bool(leftMetatable) != bool(rightMetatable) && leftMetatable != rightMetatable) { 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->retType); if (!state.errors.empty()) { reportError(expr.location, GenericError{format("Metamethod '%s' must return type 'boolean'", metamethodName.c_str())}); return errorRecoveryType(booleanType); } if (FFlag::LuauUseCommittingTxnLog) 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); if (FFlag::LuauUseCommittingTxnLog) 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}); if (!FFlag::LuauErrorRecoveryType) return errorRecoveryType(scope); } // 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 (FFlag::LuauErrorRecoveryType && 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(); if (FFlag::LuauUseCommittingTxnLog) { state.log.clear(); } else { state.DEPRECATED_log.rollback(); } state.tryUnify(actualFunctionType, fallbackFunctionType, /*isFunctionCall*/ true); if (FFlag::LuauUseCommittingTxnLog && state.errors.empty()) state.log.commit(); else if (!state.errors.empty() && !FFlag::LuauUseCommittingTxnLog) state.DEPRECATED_log.rollback(); } if (FFlag::LuauUseCommittingTxnLog && !hasErrors) { 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; } } ExprResult 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(FFlag::LuauDiscriminableUnions2 ? scope : innerScope, 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(FFlag::LuauDiscriminableUnions2 ? scope : innerScope, 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)}}; ExprResult lhs = checkExpr(scope, *expr.left, std::nullopt, /*forceSingleton=*/FFlag::LuauDiscriminableUnions2); ExprResult rhs = checkExpr(scope, *expr.right, std::nullopt, /*forceSingleton=*/FFlag::LuauDiscriminableUnions2); 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 { ExprResult lhs = checkExpr(scope, *expr.left); ExprResult rhs = checkExpr(scope, *expr.right); // Intentionally discarding predicates with other operators. return {checkBinaryOperation(scope, expr, lhs.type, rhs.type, lhs.predicates)}; } } ExprResult TypeChecker::checkExpr(const ScopePtr& scope, const AstExprTypeAssertion& expr) { TypeId annotationType = resolveType(scope, *expr.annotation); ExprResult 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)}; } ExprResult 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)}; } ExprResult TypeChecker::checkExpr(const ScopePtr& scope, const AstExprIfElse& expr, std::optional expectedType) { ExprResult result = checkExpr(scope, *expr.condition); ScopePtr trueScope = childScope(scope, expr.trueExpr->location); reportErrors(resolve(result.predicates, trueScope, true)); ExprResult trueType = checkExpr(trueScope, *expr.trueExpr, expectedType); ScopePtr falseScope = childScope(scope, expr.falseExpr->location); // Don't report errors for this scope to avoid potentially duplicating errors reported for the first scope. resolve(result.predicates, falseScope, false); ExprResult 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()) { if (!FFlag::LuauUseCommittingTxnLog) state.DEPRECATED_log.rollback(); reportError(expr.location, UnknownProperty{lhs, name}); retType = errorRecoveryType(retType); } else if (FFlag::LuauUseCommittingTxnLog) 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(FFlag::LuauAnotherTypeLevelFix ? exprTable->level : scope->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; if (get(lhsType) || get(lhsType)) return lhsType; TableTypeVar* ttv = getMutableTableType(lhsType); if (!ttv) { if (!FFlag::LuauErrorRecoveryType && !isTableIntersection(lhsType)) // This error now gets reported when we check the function body. reportError(TypeError{funName.location, OnlyTablesCanHaveMethods{lhsType}}); return errorRecoveryType(scope); } // Cannot extend sealed table, but we dont report an error here because it will be reported during AstStatFunction check if (lhsType->persistent || ttv->state == TableState::Sealed) return errorRecoveryType(scope); Name name = indexName->index.value; if (ttv->props.count(name)) return errorRecoveryType(scope); Property& property = ttv->props[name]; property.type = freshTy(); property.location = indexName->indexLocation; ttv->methodDefinitionLocations[name] = funName.location; 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 (isNonstrictMode()) retPack = anyTypePack; else if (expectedFunctionType) { auto [head, tail] = flatten(expectedFunctionType->retType); // 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; } } 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); // We explicitly don't follow here to check if we have a 'true' free type instead of bound one if (get_if(&funTy->retType->ty)) *asMutable(funTy->retType) = TypePack{{}, std::nullopt}; bool reachesImplicitReturn = getFallthrough(function.body) != nullptr; if (reachesImplicitReturn && !allowsNoReturnValues(follow(funTy->retType))) { // 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->retType}); } } } else ice("Checking non functional type"); } ExprResult 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(TypePackId tp) { size_t minCount = 0; size_t optionalCount = 0; auto it = begin(tp); 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; size_t minParams = getMinParameterCount(paramPack); if (FFlag::LuauUseCommittingTxnLog) { 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)) { 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 if (isNonstrictMode() && state.log.getMutable(t)) { } // ok else { state.reportError(TypeError{state.location, CountMismatch{minParams, paramIndex}}); 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; } // 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}; state.reportError(TypeError{location, CountMismatch{minParams, std::distance(begin(argPack), end(argPack))}}); 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)) { // 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)) { // 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}; // TODO: Better error message? state.reportError(TypeError{location, CountMismatch{minParams, std::distance(begin(argPack), end(argPack))}}); return; } } else { unifyWithInstantiationIfNeeded(scope, *argIter, *paramIter, state); ++argIter; ++paramIter; } ++paramIndex; } } else { 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 (get(*argTail)) { if (paramTail) state.tryUnify(*paramTail, *argTail); else { state.DEPRECATED_log(*argTail); *asMutable(*argTail) = TypePack{{}}; } } } else if (paramTail) { // argTail is definitely empty if (get(*paramTail)) { state.DEPRECATED_log(*paramTail); *asMutable(*paramTail) = 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 (get(tail)) { // Unify remaining parameters so we don't leave any free-types hanging around. while (paramIter != endIter) { state.tryUnify(*paramIter, errorRecoveryType(anyType)); ++paramIter; } return; } else if (auto vtp = get(tail)) { while (paramIter != endIter) { state.tryUnify(*paramIter, vtp->ty); ++paramIter; } return; } else if (get(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 = follow(*paramIter); if (isOptional(t)) { } // ok else if (get(t)) { } // ok else if (isNonstrictMode() && get(t)) { } // ok else { state.reportError(TypeError{state.location, CountMismatch{minParams, paramIndex}}); return; } ++paramIter; } } else if (paramIter == endIter) { // too many parameters passed if (!paramIter.tail()) { while (argIter != endIter) { unify(*argIter, errorRecoveryType(scope), state.location); ++argIter; } // 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}; state.reportError(TypeError{location, CountMismatch{minParams, std::distance(begin(argPack), end(argPack))}}); return; } TypePackId tail = *paramIter.tail(); if (get(tail)) { // Function is variadic. Ok. return; } else if (auto vtp = get(tail)) { // 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 (get(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(tail, varPack); return; } else if (get(tail)) { if (FFlag::LuauUseCommittingTxnLog) { state.log.replace(tail, TypePackVar(TypePack{{}})); } else { state.DEPRECATED_log(tail); *asMutable(tail) = TypePack{}; } return; } else if (get(tail)) { // 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}; // TODO: Better error message? state.reportError(TypeError{location, CountMismatch{minParams, std::distance(begin(argPack), end(argPack))}}); return; } } else { unifyWithInstantiationIfNeeded(scope, *argIter, *paramIter, state); ++argIter; ++paramIter; } ++paramIndex; } } } ExprResult 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); } actualFunctionType = follow(actualFunctionType); // 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); TypePackId retPack = freshTypePack(scope->level); std::vector> expectedTypes = getExpectedTypesForCall(overloads, expr.args.size, expr.self); ExprResult 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); if (FFlag::LuauErrorRecoveryType) { const FunctionTypeVar* overload = nullptr; if (!overloadsThatMatchArgCount.empty()) overload = get(overloadsThatMatchArgCount[0]); if (!overload && !overloadsThatDont.empty()) overload = get(overloadsThatDont[0]); if (overload) return {errorRecoveryTypePack(overload->retType)}; } 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, std::optional 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) { if (const VariadicTypePack* vtp = get(follow(*argsTail))) { while (index < argumentCount) assignOption(index++, vtp->ty); } } } } 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 ExprResult& 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 (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. TypeId r = addType(FunctionTypeVar(scope->level, argPack, retPack)); if (FFlag::LuauWidenIfSupertypeIsFree) { UnifierOptions options; options.isFunctionCall = true; unify(r, fn, expr.location, options); } else unify(fn, r, expr.location); 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->retType, /*argLocations*/ {}); if (!state.errors.empty()) { if (!FFlag::LuauUseCommittingTxnLog) state.DEPRECATED_log.rollback(); 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 if (FFlag::LuauErrorRecoveryType) overloadsThatDont.push_back(fn); errors.emplace_back(std::move(state.errors), args->head, ftv); if (!FFlag::LuauUseCommittingTxnLog) state.DEPRECATED_log.rollback(); } else { if (FFlag::LuauUseCommittingTxnLog) state.log.commit(); if (isNonstrictMode() && !expr.self && expr.func->is() && ftv->hasSelf) { // If we are running in nonstrict mode, passing fewer arguments than the function is declared to take AND // the function is declared with colon notation AND we use dot notation, warn. auto [providedArgs, providedTail] = flatten(argPack); // If we have a variadic tail, we can't say how many arguments were actually provided if (!providedTail) { std::vector actualArgs = flatten(ftv->argTypes).first; size_t providedCount = providedArgs.size(); size_t requiredCount = actualArgs.size(); // Ignore optional arguments while (providedCount < requiredCount && requiredCount != 0 && isOptional(actualArgs[requiredCount - 1])) requiredCount--; if (providedCount < requiredCount) { int requiredExtraNils = int(requiredCount - providedCount); reportError(TypeError{expr.func->location, FunctionRequiresSelf{requiredExtraNils}}); } } } 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()) { if (FFlag::LuauUseCommittingTxnLog) 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->retType, retLocations, CountMismatch::Return); return true; } else if (!FFlag::LuauUseCommittingTxnLog) editedState.DEPRECATED_log.rollback(); } 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()) { if (FFlag::LuauUseCommittingTxnLog) 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->retType, retLocations, CountMismatch::Return); return true; } else if (!FFlag::LuauUseCommittingTxnLog) editedState.DEPRECATED_log.rollback(); } } } 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->retType, {}); checkArgumentList(scope, state, argPack, ftv->argTypes, argLocations); } if (FFlag::LuauUseCommittingTxnLog && state.errors.empty()) state.log.commit(); if (i > 0) s += "; "; if (i > 0 && i == overloadTypes.size() - 1) s += "and "; s += toString(overload); if (!FFlag::LuauUseCommittingTxnLog) state.DEPRECATED_log.rollback(); } 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; } ExprResult 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. if (FFlag::LuauUseCommittingTxnLog) { inverseLogs.push_back(state.log.inverse()); state.log.commit(); } } tp->head.push_back(actualType); } } if (FFlag::LuauUseCommittingTxnLog) { for (TxnLog& log : inverseLogs) log.commit(); } else state.DEPRECATED_log.rollback(); 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; } 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) { std::string reportedModulePath = resolver->getHumanReadableModuleName(moduleInfo.name); reportError(TypeError{location, UnknownRequire{reportedModulePath}}); } return errorRecoveryType(scope); } if (module->type != SourceCode::Module) { std::string humanReadableName = resolver->getHumanReadableModuleName(moduleInfo.name); 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) { std::string humanReadableName = resolver->getHumanReadableModuleName(moduleInfo.name); reportError(location, IllegalRequire{humanReadableName, "Module does not return exactly 1 value. It cannot be required."}); return errorRecoveryType(scope); } if (FFlag::LuauImmutableTypes) return *moduleType; SeenTypes seenTypes; SeenTypePacks seenTypePacks; CloneState cloneState; return clone(*moduleType, currentModule->internalTypes, seenTypes, seenTypePacks, cloneState); } 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); if (FFlag::LuauUseCommittingTxnLog) 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); if (FFlag::LuauUseCommittingTxnLog) 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); if (FFlag::LuauUseCommittingTxnLog) 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 if (FFlag::LuauUseCommittingTxnLog) state.log.concat(std::move(child.log)); else state.DEPRECATED_log.concat(std::move(child.DEPRECATED_log)); state.errors.insert(state.errors.end(), child.errors.begin(), child.errors.end()); } else { if (!FFlag::LuauUseCommittingTxnLog) child.DEPRECATED_log.rollback(); state.tryUnify(instantiated, superTy, /*isFunctionCall*/ false); } } else { if (FFlag::LuauUseCommittingTxnLog) { state.log.concat(std::move(child.log)); } else { state.DEPRECATED_log.concat(std::move(child.DEPRECATED_log)); } } } } bool Instantiation::isDirty(TypeId ty) { if (log->getMutable(ty)) return true; else return false; } bool Instantiation::isDirty(TypePackId tp) { return false; } bool Instantiation::ignoreChildren(TypeId ty) { if (log->getMutable(ty)) return true; else return false; } TypeId Instantiation::clean(TypeId ty) { const FunctionTypeVar* ftv = log->getMutable(ty); LUAU_ASSERT(ftv); FunctionTypeVar clone = FunctionTypeVar{level, ftv->argTypes, ftv->retType, ftv->definition, ftv->hasSelf}; clone.magicFunction = ftv->magicFunction; clone.tags = ftv->tags; clone.argNames = ftv->argNames; TypeId result = addType(std::move(clone)); // Annoyingly, we have to do this even if there are no generics, // to replace any generic tables. ReplaceGenerics replaceGenerics{log, arena, level, ftv->generics, ftv->genericPacks}; // TODO: What to do if this returns nullopt? // We don't have access to the error-reporting machinery result = replaceGenerics.substitute(result).value_or(result); asMutable(result)->documentationSymbol = ty->documentationSymbol; return result; } TypePackId Instantiation::clean(TypePackId tp) { LUAU_ASSERT(false); return tp; } bool ReplaceGenerics::ignoreChildren(TypeId ty) { if (const FunctionTypeVar* ftv = log->getMutable(ty)) // We aren't recursing in the case of a generic function which // binds the same generics. This can happen if, for example, there's recursive types. // If T = (a,T)->T then instantiating T should produce T' = (X,T)->T not T' = (X,T')->T'. // It's OK to use vector equality here, since we always generate fresh generics // whenever we quantify, so the vectors overlap if and only if they are equal. return (!generics.empty() || !genericPacks.empty()) && (ftv->generics == generics) && (ftv->genericPacks == genericPacks); else return false; } bool ReplaceGenerics::isDirty(TypeId ty) { if (const TableTypeVar* ttv = log->getMutable(ty)) return ttv->state == TableState::Generic; else if (log->getMutable(ty)) return std::find(generics.begin(), generics.end(), ty) != generics.end(); else return false; } bool ReplaceGenerics::isDirty(TypePackId tp) { if (log->getMutable(tp)) return std::find(genericPacks.begin(), genericPacks.end(), tp) != genericPacks.end(); else return false; } TypeId ReplaceGenerics::clean(TypeId ty) { LUAU_ASSERT(isDirty(ty)); if (const TableTypeVar* ttv = log->getMutable(ty)) { TableTypeVar clone = TableTypeVar{ttv->props, ttv->indexer, level, TableState::Free}; clone.methodDefinitionLocations = ttv->methodDefinitionLocations; clone.definitionModuleName = ttv->definitionModuleName; return addType(std::move(clone)); } else return addType(FreeTypeVar{level}); } TypePackId ReplaceGenerics::clean(TypePackId tp) { LUAU_ASSERT(isDirty(tp)); return addTypePack(TypePackVar(FreeTypePack{level})); } bool Quantification::isDirty(TypeId ty) { if (const TableTypeVar* ttv = log->getMutable(ty)) return level.subsumes(ttv->level) && ((ttv->state == TableState::Free) || (ttv->state == TableState::Unsealed)); else if (const FreeTypeVar* ftv = log->getMutable(ty)) return level.subsumes(ftv->level); else return false; } bool Quantification::isDirty(TypePackId tp) { if (const FreeTypePack* ftv = log->getMutable(tp)) return level.subsumes(ftv->level); else return false; } TypeId Quantification::clean(TypeId ty) { LUAU_ASSERT(isDirty(ty)); if (const TableTypeVar* ttv = log->getMutable(ty)) { TableState state = (ttv->state == TableState::Unsealed ? TableState::Sealed : TableState::Generic); TableTypeVar clone = TableTypeVar{ttv->props, ttv->indexer, level, state}; clone.methodDefinitionLocations = ttv->methodDefinitionLocations; clone.definitionModuleName = ttv->definitionModuleName; return addType(std::move(clone)); } else { TypeId generic = addType(GenericTypeVar{level}); generics.push_back(generic); return generic; } } TypePackId Quantification::clean(TypePackId tp) { LUAU_ASSERT(isDirty(tp)); TypePackId genericPack = addTypePack(TypePackVar(GenericTypePack{level})); genericPacks.push_back(genericPack); return genericPack; } bool Anyification::isDirty(TypeId ty) { if (const TableTypeVar* ttv = log->getMutable(ty)) return (ttv->state == TableState::Free || (FFlag::LuauSealExports && ttv->state == TableState::Unsealed)); else if (log->getMutable(ty)) return true; else return false; } bool Anyification::isDirty(TypePackId tp) { 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.methodDefinitionLocations = ttv->methodDefinitionLocations; clone.definitionModuleName = ttv->definitionModuleName; if (FFlag::LuauSealExports) { clone.name = ttv->name; clone.syntheticName = ttv->syntheticName; clone.tags = ttv->tags; } return addType(std::move(clone)); } 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 (!ftv || !ftv->generics.empty() || !ftv->genericPacks.empty()) return ty; if (FFlag::LuauQuantifyInPlace2) { Luau::quantify(ty, scope->level); return ty; } Quantification quantification{¤tModule->internalTypes, scope->level}; std::optional qty = quantification.substitute(ty); if (!qty.has_value()) { reportError(location, UnificationTooComplex{}); return errorRecoveryType(scope); } if (ty == *qty) return ty; FunctionTypeVar* qftv = getMutable(*qty); LUAU_ASSERT(qftv); qftv->generics = std::move(quantification.generics); qftv->genericPacks = std::move(quantification.genericPacks); return *qty; } TypeId TypeChecker::instantiate(const ScopePtr& scope, TypeId ty, Location location, const TxnLog* log) { Instantiation instantiation{FFlag::LuauUseCommittingTxnLog ? log : TxnLog::empty(), ¤tModule->internalTypes, scope->level}; 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) { Anyification anyification{¤tModule->internalTypes, anyType, anyTypePack}; std::optional any = anyification.substitute(ty); if (any.has_value()) return *any; else { reportError(location, UnificationTooComplex{}); return errorRecoveryType(anyType); } } TypePackId TypeChecker::anyify(const ScopePtr& scope, TypePackId ty, Location location) { Anyification anyification{¤tModule->internalTypes, 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) { // TODO: cache singleton types return currentModule->internalTypes.addType(TypeVar(SingletonTypeVar(BooleanSingleton{value}))); } 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 eliminated otherwise 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; 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 hasDefaultTypes = false; bool hasDefaultPacks = false; bool parameterCountErrorReported = false; if (FFlag::LuauTypeAliasDefaults) { hasDefaultTypes = std::any_of(tf->typeParams.begin(), tf->typeParams.end(), [](auto&& el) { return el.defaultValue.has_value(); }); 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; if (!FFlag::LuauErrorRecoveryType) return errorRecoveryType(scope); } } } else { if (!lit->hasParameterList && !tf->typePackParams.empty()) { reportError(TypeError{annotation.location, GenericError{"Type parameter list is required"}}); if (!FFlag::LuauErrorRecoveryType) return errorRecoveryType(scope); } } 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)); if (FFlag::LuauTypeAliasDefaults) { 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()}}); if (FFlag::LuauErrorRecoveryType) { // 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)); } else return errorRecoveryType(scope); } if (FFlag::LuauRecursiveTypeParameterRestriction) { 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)); return addType(TableTypeVar{ props, tableIndexer, scope->level, TableState::Sealed // FIXME: probably want a way to annotate other kinds of tables maybe }); } 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) { // Really this should just replace the arguments, // but for bug-compatibility with existing code, we replace // all generics. if (get(ty)) return true; else if (const FreeTypeVar* ftv = get(ty)) { if (FFlag::LuauRecursiveTypeParameterRestriction && ftv->forwardedTypeAlias) encounteredForwardedType = true; return false; } else return false; } bool ApplyTypeFunction::isDirty(TypePackId tp) { // Really this should just replace the arguments, // but for bug-compatibility with existing code, we replace // all generics. if (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) { // Really this should just replace the arguments, // but for bug-compatibility with existing code, we replace // all generics by free type variables. TypeId& arg = typeArguments[ty]; if (arg) return arg; else return addType(FreeTypeVar{level}); } TypePackId ApplyTypeFunction::clean(TypePackId tp) { // Really this should just replace the arguments, // but for bug-compatibility with existing code, we replace // all generics by free type variables. TypePackId& arg = typePackArguments[tp]; 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 (FFlag::LuauRecursiveTypeParameterRestriction && applyTypeFunction.encounteredForwardedType) { reportError(TypeError{location, GenericError{"Recursive type being used with different parameters"}}); return errorRecoveryType(scope); } TypeId instantiated = *maybeInstantiated; // TODO: CLI-46926 it's not a good idea to rename the type here TypeId target = follow(instantiated); bool needsClone = follow(tf.type) == target; TableTypeVar* ttv = getMutableTableType(target); if (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 (ttv) { ttv->instantiatedTypeParams = typeParams; ttv->instantiatedTypePackParams = typePackParams; } 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 = (FFlag::LuauQuantifyInPlace2 && levelOpt) ? *levelOpt : scope->level; std::vector generics; for (const AstGenericType& generic : genericNames) { std::optional defaultValue; if (FFlag::LuauTypeAliasDefaults && 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 (FFlag::LuauRecursiveTypeParameterRestriction && (!FFlag::LuauGenericFunctionsDontCacheTypeParams || 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 (FFlag::LuauTypeAliasDefaults && 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 g; if (FFlag::LuauRecursiveTypeParameterRestriction) { TypePackId& cached = scope->parent->typeAliasTypePackParameters[n]; if (!cached) cached = addTypePack(TypePackVar{Unifiable::Generic{level, n}}); g = cached; } else { g = addTypePack(TypePackVar{Unifiable::Generic{level, n}}); } genericPacks.push_back({g, defaultValue}); scope->privateTypePackBindings[n] = g; } return {generics, genericPacks}; } void TypeChecker::refineLValue(const LValue& lvalue, RefinementMap& refis, const ScopePtr& scope, TypeIdPredicate predicate) { LUAU_ASSERT(FFlag::LuauDiscriminableUnions2 || FFlag::LuauAssertStripsFalsyTypes); 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 auto& [symbol, keys] = getFullName(lvalue); ScopePtr currentScope = scope; while (currentScope) { std::optional found; std::vector childKeys; const LValue* currentLValue = &lvalue; while (currentLValue) { if (auto it = currentScope->refinements.find(*currentLValue); it != currentScope->refinements.end()) { found = it->second; break; } childKeys.push_back(*currentLValue); currentLValue = baseof(*currentLValue); } 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; } } 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); } ErrorVec TypeChecker::resolve(const PredicateVec& predicates, const ScopePtr& scope, bool sense) { ErrorVec errVec; resolve(predicates, errVec, scope->refinements, scope, sense); return errVec; } void TypeChecker::resolve(const PredicateVec& predicates, ErrorVec& errVec, RefinementMap& refis, const ScopePtr& scope, bool sense, bool fromOr) { for (const Predicate& c : predicates) resolve(c, errVec, refis, scope, sense, fromOr); } void TypeChecker::resolve(const Predicate& predicate, ErrorVec& errVec, RefinementMap& refis, const ScopePtr& scope, bool sense, bool fromOr) { if (auto truthyP = get(predicate)) resolve(*truthyP, errVec, refis, scope, sense, fromOr); else if (auto andP = get(predicate)) resolve(*andP, errVec, refis, scope, sense); else if (auto orP = get(predicate)) resolve(*orP, errVec, refis, scope, sense); else if (auto notP = get(predicate)) resolve(notP->predicates, errVec, refis, scope, !sense, fromOr); else if (auto isaP = get(predicate)) resolve(*isaP, errVec, refis, scope, sense); else if (auto typeguardP = get(predicate)) resolve(*typeguardP, errVec, refis, scope, sense); else if (auto eqP = get(predicate)) resolve(*eqP, errVec, refis, scope, sense); else ice("Unhandled predicate kind"); } void TypeChecker::resolve(const TruthyPredicate& truthyP, ErrorVec& errVec, RefinementMap& refis, const ScopePtr& scope, bool sense, bool fromOr) { if (FFlag::LuauAssertStripsFalsyTypes) { std::optional ty = resolveLValue(refis, scope, truthyP.lvalue); if (ty && fromOr) return addRefinement(refis, truthyP.lvalue, *ty); refineLValue(truthyP.lvalue, refis, scope, mkTruthyPredicate(sense)); } else { auto predicate = [sense](TypeId option) -> std::optional { if (isUndecidable(option) || isBoolean(option) || isNil(option) != sense) return option; return std::nullopt; }; if (FFlag::LuauDiscriminableUnions2) { std::optional ty = resolveLValue(refis, scope, truthyP.lvalue); if (ty && fromOr) return addRefinement(refis, truthyP.lvalue, *ty); refineLValue(truthyP.lvalue, refis, scope, predicate); } else { std::optional ty = resolveLValue(refis, scope, truthyP.lvalue); if (!ty) return; // This is a hack. :( // Without this, the expression 'a or b' might refine 'b' to be falsy. // I'm not yet sure how else to get this to do the right thing without this hack, so we'll do this for now in the meantime. if (fromOr) return addRefinement(refis, truthyP.lvalue, *ty); if (std::optional result = filterMap(*ty, predicate)) addRefinement(refis, truthyP.lvalue, *result); } } } void TypeChecker::resolve(const AndPredicate& andP, ErrorVec& errVec, RefinementMap& refis, const ScopePtr& scope, bool sense) { if (!sense) { OrPredicate orP{ {NotPredicate{std::move(andP.lhs)}}, {NotPredicate{std::move(andP.rhs)}}, }; return resolve(orP, errVec, refis, scope, !sense); } resolve(andP.lhs, errVec, refis, scope, sense); resolve(andP.rhs, errVec, refis, scope, sense); } void TypeChecker::resolve(const OrPredicate& orP, ErrorVec& errVec, RefinementMap& refis, const ScopePtr& scope, bool sense) { if (!sense) { AndPredicate andP{ {NotPredicate{std::move(orP.lhs)}}, {NotPredicate{std::move(orP.rhs)}}, }; return resolve(andP, errVec, refis, scope, !sense); } ErrorVec discarded; RefinementMap leftRefis; resolve(orP.lhs, errVec, leftRefis, scope, sense); RefinementMap rightRefis; resolve(orP.lhs, discarded, rightRefis, scope, !sense); resolve(orP.rhs, errVec, rightRefis, scope, sense, true); // :( merge(refis, leftRefis); merge(refis, rightRefis); } void TypeChecker::resolve(const IsAPredicate& isaP, ErrorVec& errVec, 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. // The best unambiguous way about this would be to have a function that returns the relationship ordering of a pair. // i.e. TypeRelationship relationshipOf(TypeId superTy, TypeId subTy) 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; }; if (FFlag::LuauDiscriminableUnions2) { refineLValue(isaP.lvalue, refis, scope, predicate); } else { std::optional ty = resolveLValue(refis, scope, isaP.lvalue); if (!ty) return; if (std::optional result = filterMap(*ty, predicate)) addRefinement(refis, isaP.lvalue, *result); else { addRefinement(refis, isaP.lvalue, errorRecoveryType(scope)); errVec.push_back(TypeError{isaP.location, TypeMismatch{isaP.ty, *ty}}); } } } void TypeChecker::resolve(const TypeGuardPredicate& typeguardP, ErrorVec& errVec, 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}, errVec, 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()) { if (FFlag::LuauDiscriminableUnions2) { refineLValue(typeguardP.lvalue, refis, scope, it->second(sense)); return; } else { if (std::optional result = filterMap(*ty, it->second(sense))) addRefinement(refis, typeguardP.lvalue, *result); else { addRefinement(refis, typeguardP.lvalue, errorRecoveryType(scope)); if (sense) errVec.push_back( TypeError{typeguardP.location, GenericError{"Type '" + toString(*ty) + "' has no overlap with '" + typeguardP.kind + "'"}}); } return; } } auto fail = [&](const TypeErrorData& err) { if (!FFlag::LuauDiscriminableUnions2) errVec.push_back(TypeError{typeguardP.location, err}); addRefinement(refis, typeguardP.lvalue, errorRecoveryType(scope)); }; if (!typeguardP.isTypeof) return fail(UnknownSymbol{typeguardP.kind, UnknownSymbol::Type}); auto typeFun = globalScope->lookupType(typeguardP.kind); if (!typeFun || !typeFun->typeParams.empty() || !typeFun->typePackParams.empty()) return fail(UnknownSymbol{typeguardP.kind, UnknownSymbol::Type}); 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 fail(UnknownSymbol{typeguardP.kind, UnknownSymbol::Type}); // 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}, errVec, refis, scope, sense); } void TypeChecker::resolve(const EqPredicate& eqP, ErrorVec& errVec, 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}; }; if (FFlag::LuauDiscriminableUnions2) { 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 && isUndecidable(option)) return FFlag::LuauWeakEqConstraint ? option : eqP.type; 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); } else { if (FFlag::LuauWeakEqConstraint) { if (!sense && isNil(eqP.type)) resolve(TruthyPredicate{std::move(eqP.lvalue), eqP.location}, errVec, refis, scope, true, /* fromOr= */ false); return; } if (FFlag::LuauEqConstraint) { std::optional ty = resolveLValue(refis, scope, eqP.lvalue); if (!ty) return; std::vector lhs = options(*ty); std::vector rhs = options(eqP.type); if (sense && std::any_of(lhs.begin(), lhs.end(), isUndecidable)) { addRefinement(refis, eqP.lvalue, eqP.type); return; } else 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. std::unordered_set set; for (TypeId left : lhs) { for (TypeId right : rhs) { // When singleton types arrive, `isNil` here probably should be replaced with `isLiteral`. if (canUnify(right, left, eqP.location).empty() == sense || (!sense && !isNil(left))) set.insert(left); } } if (set.empty()) return; std::vector viable(set.begin(), set.end()); TypeId result = viable.size() == 1 ? viable[0] : addType(UnionTypeVar{std::move(viable)}); addRefinement(refis, eqP.lvalue, result); } } } bool TypeChecker::isNonstrictMode() const { return (currentModule->mode == Mode::Nonstrict) || (currentModule->mode == Mode::NoCheck); } 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