// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details #include "Luau/Anyification.h" #include "Luau/ApplyTypeFunction.h" #include "Luau/Clone.h" #include "Luau/ConstraintSolver.h" #include "Luau/DcrLogger.h" #include "Luau/Instantiation.h" #include "Luau/Location.h" #include "Luau/Metamethods.h" #include "Luau/ModuleResolver.h" #include "Luau/Quantify.h" #include "Luau/ToString.h" #include "Luau/TypeUtils.h" #include "Luau/TypeVar.h" #include "Luau/Unifier.h" #include "Luau/VisitTypeVar.h" #include "Luau/TypeUtils.h" LUAU_FASTFLAGVARIABLE(DebugLuauLogSolver, false); LUAU_FASTFLAGVARIABLE(DebugLuauLogSolverToJson, false); namespace Luau { [[maybe_unused]] static void dumpBindings(NotNull scope, ToStringOptions& opts) { for (const auto& [k, v] : scope->bindings) { auto d = toString(v.typeId, opts); printf("\t%s : %s\n", k.c_str(), d.c_str()); } for (NotNull child : scope->children) dumpBindings(child, opts); } static std::pair, std::vector> saturateArguments(TypeArena* arena, NotNull singletonTypes, const TypeFun& fn, const std::vector& rawTypeArguments, const std::vector& rawPackArguments) { std::vector saturatedTypeArguments; std::vector extraTypes; std::vector saturatedPackArguments; for (size_t i = 0; i < rawTypeArguments.size(); ++i) { TypeId ty = rawTypeArguments[i]; if (i < fn.typeParams.size()) saturatedTypeArguments.push_back(ty); else extraTypes.push_back(ty); } // If we collected extra types, put them in a type pack now. This case is // mutually exclusive with the type pack -> type conversion we do below: // extraTypes will only have elements in it if we have more types than we // have parameter slots for them to go into. if (!extraTypes.empty()) { saturatedPackArguments.push_back(arena->addTypePack(extraTypes)); } for (size_t i = 0; i < rawPackArguments.size(); ++i) { TypePackId tp = rawPackArguments[i]; // If we are short on regular type saturatedTypeArguments and we have a single // element type pack, we can decompose that to the type it contains and // use that as a type parameter. if (saturatedTypeArguments.size() < fn.typeParams.size() && size(tp) == 1 && finite(tp) && first(tp) && saturatedPackArguments.empty()) { saturatedTypeArguments.push_back(*first(tp)); } else { saturatedPackArguments.push_back(tp); } } size_t typesProvided = saturatedTypeArguments.size(); size_t typesRequired = fn.typeParams.size(); size_t packsProvided = saturatedPackArguments.size(); size_t packsRequired = fn.typePackParams.size(); // Extra types should be accumulated in extraTypes, not saturatedTypeArguments. Extra // packs will be accumulated in saturatedPackArguments, so we don't have an // assertion for that. LUAU_ASSERT(typesProvided <= typesRequired); // If we didn't provide enough types, but we did provide a type pack, we // don't want to use defaults. The rationale for this is that if the user // provides a pack but doesn't provide enough types, we want to report an // error, rather than simply using the default saturatedTypeArguments, if they exist. If // they did provide enough types, but not enough packs, we of course want to // use the default packs. bool needsDefaults = (typesProvided < typesRequired && packsProvided == 0) || (typesProvided == typesRequired && packsProvided < packsRequired); if (needsDefaults) { // Default types can reference earlier types. It's legal to write // something like // type T = (A, B) -> number // and we need to respect that. We use an ApplyTypeFunction for this. ApplyTypeFunction atf{arena}; for (size_t i = 0; i < typesProvided; ++i) atf.typeArguments[fn.typeParams[i].ty] = saturatedTypeArguments[i]; for (size_t i = typesProvided; i < typesRequired; ++i) { TypeId defaultTy = fn.typeParams[i].defaultValue.value_or(nullptr); // We will fill this in with the error type later. if (!defaultTy) break; TypeId instantiatedDefault = atf.substitute(defaultTy).value_or(singletonTypes->errorRecoveryType()); atf.typeArguments[fn.typeParams[i].ty] = instantiatedDefault; saturatedTypeArguments.push_back(instantiatedDefault); } for (size_t i = 0; i < packsProvided; ++i) { atf.typePackArguments[fn.typePackParams[i].tp] = saturatedPackArguments[i]; } for (size_t i = packsProvided; i < packsRequired; ++i) { TypePackId defaultTp = fn.typePackParams[i].defaultValue.value_or(nullptr); // We will fill this in with the error type pack later. if (!defaultTp) break; TypePackId instantiatedDefault = atf.substitute(defaultTp).value_or(singletonTypes->errorRecoveryTypePack()); atf.typePackArguments[fn.typePackParams[i].tp] = instantiatedDefault; saturatedPackArguments.push_back(instantiatedDefault); } } // If we didn't create an extra type pack from overflowing parameter packs, // and we're still missing a type pack, plug in an empty type pack as the // value of the empty packs. if (extraTypes.empty() && saturatedPackArguments.size() + 1 == fn.typePackParams.size()) { saturatedPackArguments.push_back(arena->addTypePack({})); } // We need to have _something_ when we substitute the generic saturatedTypeArguments, // even if they're missing, so we use the error type as a filler. for (size_t i = saturatedTypeArguments.size(); i < typesRequired; ++i) { saturatedTypeArguments.push_back(singletonTypes->errorRecoveryType()); } for (size_t i = saturatedPackArguments.size(); i < packsRequired; ++i) { saturatedPackArguments.push_back(singletonTypes->errorRecoveryTypePack()); } // At this point, these two conditions should be true. If they aren't we // will run into access violations. LUAU_ASSERT(saturatedTypeArguments.size() == fn.typeParams.size()); LUAU_ASSERT(saturatedPackArguments.size() == fn.typePackParams.size()); return {saturatedTypeArguments, saturatedPackArguments}; } bool InstantiationSignature::operator==(const InstantiationSignature& rhs) const { return fn == rhs.fn && arguments == rhs.arguments && packArguments == rhs.packArguments; } size_t HashInstantiationSignature::operator()(const InstantiationSignature& signature) const { size_t hash = std::hash{}(signature.fn.type); for (const GenericTypeDefinition& p : signature.fn.typeParams) { hash ^= (std::hash{}(p.ty) << 1); } for (const GenericTypePackDefinition& p : signature.fn.typePackParams) { hash ^= (std::hash{}(p.tp) << 1); } for (const TypeId a : signature.arguments) { hash ^= (std::hash{}(a) << 1); } for (const TypePackId a : signature.packArguments) { hash ^= (std::hash{}(a) << 1); } return hash; } void dump(ConstraintSolver* cs, ToStringOptions& opts) { printf("constraints:\n"); for (NotNull c : cs->unsolvedConstraints) { auto it = cs->blockedConstraints.find(c); int blockCount = it == cs->blockedConstraints.end() ? 0 : int(it->second); printf("\t%d\t%s\n", blockCount, toString(*c, opts).c_str()); for (NotNull dep : c->dependencies) { auto unsolvedIter = std::find(begin(cs->unsolvedConstraints), end(cs->unsolvedConstraints), dep); if (unsolvedIter == cs->unsolvedConstraints.end()) continue; auto it = cs->blockedConstraints.find(dep); int blockCount = it == cs->blockedConstraints.end() ? 0 : int(it->second); printf("\t%d\t\t%s\n", blockCount, toString(*dep, opts).c_str()); } if (auto fcc = get(*c)) { for (NotNull inner : fcc->innerConstraints) printf("\t ->\t\t%s\n", toString(*inner, opts).c_str()); } } } ConstraintSolver::ConstraintSolver(NotNull normalizer, NotNull rootScope, std::vector> constraints, ModuleName moduleName, NotNull moduleResolver, std::vector requireCycles, DcrLogger* logger) : arena(normalizer->arena) , singletonTypes(normalizer->singletonTypes) , normalizer(normalizer) , constraints(std::move(constraints)) , rootScope(rootScope) , currentModuleName(std::move(moduleName)) , moduleResolver(moduleResolver) , requireCycles(requireCycles) , logger(logger) { opts.exhaustive = true; for (NotNull c : this->constraints) { unsolvedConstraints.push_back(c); for (NotNull dep : c->dependencies) { block(dep, c); } } if (FFlag::DebugLuauLogSolverToJson) LUAU_ASSERT(logger); } void ConstraintSolver::randomize(unsigned seed) { if (unsolvedConstraints.empty()) return; unsigned int rng = seed; for (size_t i = unsolvedConstraints.size() - 1; i > 0; --i) { // Fisher-Yates shuffle size_t j = rng % (i + 1); std::swap(unsolvedConstraints[i], unsolvedConstraints[j]); // LCG RNG, constants from Numerical Recipes // This may occasionally result in skewed shuffles due to distribution properties, but this is a debugging tool so it should be good enough rng = rng * 1664525 + 1013904223; } } void ConstraintSolver::run() { if (isDone()) return; if (FFlag::DebugLuauLogSolver) { printf("Starting solver\n"); dump(this, opts); printf("Bindings:\n"); dumpBindings(rootScope, opts); } if (FFlag::DebugLuauLogSolverToJson) { logger->captureInitialSolverState(rootScope, unsolvedConstraints); } auto runSolverPass = [&](bool force) { bool progress = false; size_t i = 0; while (i < unsolvedConstraints.size()) { NotNull c = unsolvedConstraints[i]; if (!force && isBlocked(c)) { ++i; continue; } std::string saveMe = FFlag::DebugLuauLogSolver ? toString(*c, opts) : std::string{}; StepSnapshot snapshot; if (FFlag::DebugLuauLogSolverToJson) { snapshot = logger->prepareStepSnapshot(rootScope, c, force, unsolvedConstraints); } bool success = tryDispatch(c, force); progress |= success; if (success) { unblock(c); unsolvedConstraints.erase(unsolvedConstraints.begin() + i); if (FFlag::DebugLuauLogSolverToJson) { logger->commitStepSnapshot(snapshot); } if (FFlag::DebugLuauLogSolver) { if (force) printf("Force "); printf("Dispatched\n\t%s\n", saveMe.c_str()); dump(this, opts); } } else ++i; if (force && success) return true; } return progress; }; bool progress = false; do { progress = runSolverPass(false); if (!progress) progress |= runSolverPass(true); } while (progress); finalizeModule(); if (FFlag::DebugLuauLogSolver) { dumpBindings(rootScope, opts); } if (FFlag::DebugLuauLogSolverToJson) { logger->captureFinalSolverState(rootScope, unsolvedConstraints); } } bool ConstraintSolver::isDone() { return unsolvedConstraints.empty(); } void ConstraintSolver::finalizeModule() { Anyification a{arena, rootScope, singletonTypes, &iceReporter, singletonTypes->anyType, singletonTypes->anyTypePack}; std::optional returnType = a.substitute(rootScope->returnType); if (!returnType) { reportError(CodeTooComplex{}, Location{}); rootScope->returnType = singletonTypes->errorTypePack; } else rootScope->returnType = *returnType; } bool ConstraintSolver::tryDispatch(NotNull constraint, bool force) { if (!force && isBlocked(constraint)) return false; bool success = false; if (auto sc = get(*constraint)) success = tryDispatch(*sc, constraint, force); else if (auto psc = get(*constraint)) success = tryDispatch(*psc, constraint, force); else if (auto gc = get(*constraint)) success = tryDispatch(*gc, constraint, force); else if (auto ic = get(*constraint)) success = tryDispatch(*ic, constraint, force); else if (auto uc = get(*constraint)) success = tryDispatch(*uc, constraint, force); else if (auto bc = get(*constraint)) success = tryDispatch(*bc, constraint, force); else if (auto ic = get(*constraint)) success = tryDispatch(*ic, constraint, force); else if (auto nc = get(*constraint)) success = tryDispatch(*nc, constraint); else if (auto taec = get(*constraint)) success = tryDispatch(*taec, constraint); else if (auto fcc = get(*constraint)) success = tryDispatch(*fcc, constraint); else if (auto fcc = get(*constraint)) success = tryDispatch(*fcc, constraint); else if (auto hpc = get(*constraint)) success = tryDispatch(*hpc, constraint); else if (auto spc = get(*constraint)) success = tryDispatch(*spc, constraint); else if (auto sottc = get(*constraint)) success = tryDispatch(*sottc, constraint); else LUAU_ASSERT(false); if (success) { unblock(constraint); } return success; } bool ConstraintSolver::tryDispatch(const SubtypeConstraint& c, NotNull constraint, bool force) { if (!recursiveBlock(c.subType, constraint)) return false; if (!recursiveBlock(c.superType, constraint)) return false; if (isBlocked(c.subType)) return block(c.subType, constraint); else if (isBlocked(c.superType)) return block(c.superType, constraint); unify(c.subType, c.superType, constraint->scope); return true; } bool ConstraintSolver::tryDispatch(const PackSubtypeConstraint& c, NotNull constraint, bool force) { if (!recursiveBlock(c.subPack, constraint) || !recursiveBlock(c.superPack, constraint)) return false; if (isBlocked(c.subPack)) return block(c.subPack, constraint); else if (isBlocked(c.superPack)) return block(c.superPack, constraint); unify(c.subPack, c.superPack, constraint->scope); return true; } bool ConstraintSolver::tryDispatch(const GeneralizationConstraint& c, NotNull constraint, bool force) { if (isBlocked(c.sourceType)) return block(c.sourceType, constraint); TypeId generalized = quantify(arena, c.sourceType, constraint->scope); if (isBlocked(c.generalizedType)) asMutable(c.generalizedType)->ty.emplace(generalized); else unify(c.generalizedType, generalized, constraint->scope); unblock(c.generalizedType); unblock(c.sourceType); return true; } bool ConstraintSolver::tryDispatch(const InstantiationConstraint& c, NotNull constraint, bool force) { if (isBlocked(c.superType)) return block(c.superType, constraint); Instantiation inst(TxnLog::empty(), arena, TypeLevel{}, constraint->scope); std::optional instantiated = inst.substitute(c.superType); LUAU_ASSERT(instantiated); // TODO FIXME HANDLE THIS if (isBlocked(c.subType)) asMutable(c.subType)->ty.emplace(*instantiated); else unify(c.subType, *instantiated, constraint->scope); unblock(c.subType); return true; } bool ConstraintSolver::tryDispatch(const UnaryConstraint& c, NotNull constraint, bool force) { TypeId operandType = follow(c.operandType); if (isBlocked(operandType)) return block(operandType, constraint); if (get(operandType)) return block(operandType, constraint); LUAU_ASSERT(get(c.resultType)); switch (c.op) { case AstExprUnary::Not: { asMutable(c.resultType)->ty.emplace(singletonTypes->booleanType); return true; } case AstExprUnary::Len: { // __len must return a number. asMutable(c.resultType)->ty.emplace(singletonTypes->numberType); return true; } case AstExprUnary::Minus: { if (isNumber(operandType) || get(operandType) || get(operandType)) { asMutable(c.resultType)->ty.emplace(c.operandType); } else if (std::optional mm = findMetatableEntry(singletonTypes, errors, operandType, "__unm", constraint->location)) { const FunctionTypeVar* ftv = get(follow(*mm)); if (!ftv) { if (std::optional callMm = findMetatableEntry(singletonTypes, errors, follow(*mm), "__call", constraint->location)) { ftv = get(follow(*callMm)); } } if (!ftv) { asMutable(c.resultType)->ty.emplace(singletonTypes->errorRecoveryType()); return true; } TypePackId argsPack = arena->addTypePack({operandType}); unify(ftv->argTypes, argsPack, constraint->scope); TypeId result = singletonTypes->errorRecoveryType(); if (ftv) { result = first(ftv->retTypes).value_or(singletonTypes->errorRecoveryType()); } asMutable(c.resultType)->ty.emplace(result); } else { asMutable(c.resultType)->ty.emplace(singletonTypes->errorRecoveryType()); } return true; } } LUAU_ASSERT(false); return false; } bool ConstraintSolver::tryDispatch(const BinaryConstraint& c, NotNull constraint, bool force) { TypeId leftType = follow(c.leftType); TypeId rightType = follow(c.rightType); TypeId resultType = follow(c.resultType); bool isLogical = c.op == AstExprBinary::Op::And || c.op == AstExprBinary::Op::Or; /* Compound assignments create constraints of the form * * A <: Binary * * This constraint is the one that is meant to unblock A, so it doesn't * make any sense to stop and wait for someone else to do it. */ if (isBlocked(leftType) && leftType != resultType) return block(c.leftType, constraint); if (isBlocked(rightType) && rightType != resultType) return block(c.rightType, constraint); if (!force) { // Logical expressions may proceed if the LHS is free. if (get(leftType) && !isLogical) return block(leftType, constraint); } // Logical expressions may proceed if the LHS is free. if (isBlocked(leftType) || (get(leftType) && !isLogical)) { asMutable(resultType)->ty.emplace(errorRecoveryType()); unblock(resultType); return true; } // Metatables go first, even if there is primitive behavior. if (auto it = kBinaryOpMetamethods.find(c.op); it != kBinaryOpMetamethods.end()) { // Metatables are not the same. The metamethod will not be invoked. if ((c.op == AstExprBinary::Op::CompareEq || c.op == AstExprBinary::Op::CompareNe) && getMetatable(leftType, singletonTypes) != getMetatable(rightType, singletonTypes)) { // TODO: Boolean singleton false? The result is _always_ boolean false. asMutable(resultType)->ty.emplace(singletonTypes->booleanType); unblock(resultType); return true; } std::optional mm; // The LHS metatable takes priority over the RHS metatable, where // present. if (std::optional leftMm = findMetatableEntry(singletonTypes, errors, leftType, it->second, constraint->location)) mm = leftMm; else if (std::optional rightMm = findMetatableEntry(singletonTypes, errors, rightType, it->second, constraint->location)) mm = rightMm; if (mm) { // TODO: Is a table with __call legal here? // TODO: Overloads if (const FunctionTypeVar* ftv = get(follow(*mm))) { TypePackId inferredArgs; // For >= and > we invoke __lt and __le respectively with // swapped argument ordering. if (c.op == AstExprBinary::Op::CompareGe || c.op == AstExprBinary::Op::CompareGt) { inferredArgs = arena->addTypePack({rightType, leftType}); } else { inferredArgs = arena->addTypePack({leftType, rightType}); } unify(inferredArgs, ftv->argTypes, constraint->scope); TypeId mmResult; // Comparison operations always evaluate to a boolean, // regardless of what the metamethod returns. switch (c.op) { case AstExprBinary::Op::CompareEq: case AstExprBinary::Op::CompareNe: case AstExprBinary::Op::CompareGe: case AstExprBinary::Op::CompareGt: case AstExprBinary::Op::CompareLe: case AstExprBinary::Op::CompareLt: mmResult = singletonTypes->booleanType; break; default: mmResult = first(ftv->retTypes).value_or(errorRecoveryType()); } asMutable(resultType)->ty.emplace(mmResult); unblock(resultType); return true; } } // If there's no metamethod available, fall back to primitive behavior. } // If any is present, the expression must evaluate to any as well. bool leftAny = get(leftType) || get(leftType); bool rightAny = get(rightType) || get(rightType); bool anyPresent = leftAny || rightAny; switch (c.op) { // For arithmetic operators, if the LHS is a number, the RHS must be a // number as well. The result will also be a number. case AstExprBinary::Op::Add: case AstExprBinary::Op::Sub: case AstExprBinary::Op::Mul: case AstExprBinary::Op::Div: case AstExprBinary::Op::Pow: case AstExprBinary::Op::Mod: if (isNumber(leftType)) { unify(leftType, rightType, constraint->scope); asMutable(resultType)->ty.emplace(anyPresent ? singletonTypes->anyType : leftType); unblock(resultType); return true; } break; // For concatenation, if the LHS is a string, the RHS must be a string as // well. The result will also be a string. case AstExprBinary::Op::Concat: if (isString(leftType)) { unify(leftType, rightType, constraint->scope); asMutable(resultType)->ty.emplace(anyPresent ? singletonTypes->anyType : leftType); unblock(resultType); return true; } break; // Inexact comparisons require that the types be both numbers or both // strings, and evaluate to a boolean. case AstExprBinary::Op::CompareGe: case AstExprBinary::Op::CompareGt: case AstExprBinary::Op::CompareLe: case AstExprBinary::Op::CompareLt: if ((isNumber(leftType) && isNumber(rightType)) || (isString(leftType) && isString(rightType))) { asMutable(resultType)->ty.emplace(singletonTypes->booleanType); unblock(resultType); return true; } break; // == and ~= always evaluate to a boolean, and impose no other constraints // on their parameters. case AstExprBinary::Op::CompareEq: case AstExprBinary::Op::CompareNe: asMutable(resultType)->ty.emplace(singletonTypes->booleanType); unblock(resultType); return true; // And evalutes to a boolean if the LHS is falsey, and the RHS type if LHS is // truthy. case AstExprBinary::Op::And: { TypeId leftFilteredTy = arena->addType(IntersectionTypeVar{{singletonTypes->falsyType, leftType}}); // TODO: normaliztion here should be replaced by a more limited 'simplification' const NormalizedType* normalized = normalizer->normalize(arena->addType(UnionTypeVar{{leftFilteredTy, rightType}})); if (!normalized) { reportError(CodeTooComplex{}, constraint->location); asMutable(resultType)->ty.emplace(errorRecoveryType()); } else { asMutable(resultType)->ty.emplace(normalizer->typeFromNormal(*normalized)); } unblock(resultType); return true; } // Or evaluates to the LHS type if the LHS is truthy, and the RHS type if // LHS is falsey. case AstExprBinary::Op::Or: { TypeId rightFilteredTy = arena->addType(IntersectionTypeVar{{singletonTypes->truthyType, leftType}}); // TODO: normaliztion here should be replaced by a more limited 'simplification' const NormalizedType* normalized = normalizer->normalize(arena->addType(UnionTypeVar{{rightFilteredTy, rightType}})); if (!normalized) { reportError(CodeTooComplex{}, constraint->location); asMutable(resultType)->ty.emplace(errorRecoveryType()); } else { asMutable(resultType)->ty.emplace(normalizer->typeFromNormal(*normalized)); } unblock(resultType); return true; } default: iceReporter.ice("Unhandled AstExprBinary::Op for binary operation", constraint->location); break; } // We failed to either evaluate a metamethod or invoke primitive behavior. unify(leftType, errorRecoveryType(), constraint->scope); unify(rightType, errorRecoveryType(), constraint->scope); asMutable(resultType)->ty.emplace(errorRecoveryType()); unblock(resultType); return true; } bool ConstraintSolver::tryDispatch(const IterableConstraint& c, NotNull constraint, bool force) { /* * for .. in loops can play out in a bunch of different ways depending on * the shape of iteratee. * * iteratee might be: * * (nextFn) * * (nextFn, table) * * (nextFn, table, firstIndex) * * table with a metatable and __index * * table with a metatable and __call but no __index (if the metatable has * both, __index takes precedence) * * table with an indexer but no __index or __call (or no metatable) * * To dispatch this constraint, we need first to know enough about iteratee * to figure out which of the above shapes we are actually working with. * * If `force` is true and we still do not know, we must flag a warning. Type * families are the fix for this. * * Since we need to know all of this stuff about the types of the iteratee, * we have no choice but for ConstraintSolver to also be the thing that * applies constraints to the types of the iterators. */ auto block_ = [&](auto&& t) { if (force) { // If we haven't figured out the type of the iteratee by now, // there's nothing we can do. return true; } block(t, constraint); return false; }; auto [iteratorTypes, iteratorTail] = flatten(c.iterator); if (iteratorTail) return block_(*iteratorTail); { bool blocked = false; for (TypeId t : iteratorTypes) { if (isBlocked(t)) { block(t, constraint); blocked = true; } } if (blocked) return false; } if (0 == iteratorTypes.size()) { Anyification anyify{arena, constraint->scope, singletonTypes, &iceReporter, errorRecoveryType(), errorRecoveryTypePack()}; std::optional anyified = anyify.substitute(c.variables); LUAU_ASSERT(anyified); unify(*anyified, c.variables, constraint->scope); return true; } TypeId nextTy = follow(iteratorTypes[0]); if (get(nextTy)) return block_(nextTy); if (get(nextTy)) { TypeId tableTy = singletonTypes->nilType; if (iteratorTypes.size() >= 2) tableTy = iteratorTypes[1]; TypeId firstIndexTy = singletonTypes->nilType; if (iteratorTypes.size() >= 3) firstIndexTy = iteratorTypes[2]; return tryDispatchIterableFunction(nextTy, tableTy, firstIndexTy, c, constraint, force); } else return tryDispatchIterableTable(iteratorTypes[0], c, constraint, force); return true; } bool ConstraintSolver::tryDispatch(const NameConstraint& c, NotNull constraint) { if (isBlocked(c.namedType)) return block(c.namedType, constraint); TypeId target = follow(c.namedType); if (target->persistent || target->owningArena != arena) return true; if (TableTypeVar* ttv = getMutable(target)) ttv->name = c.name; else if (MetatableTypeVar* mtv = getMutable(target)) mtv->syntheticName = c.name; else if (get(target) || get(target)) { // nothing (yet) } else return block(c.namedType, constraint); return true; } struct InfiniteTypeFinder : TypeVarOnceVisitor { ConstraintSolver* solver; const InstantiationSignature& signature; NotNull scope; bool foundInfiniteType = false; explicit InfiniteTypeFinder(ConstraintSolver* solver, const InstantiationSignature& signature, NotNull scope) : solver(solver) , signature(signature) , scope(scope) { } bool visit(TypeId ty, const PendingExpansionTypeVar& petv) override { std::optional tf = (petv.prefix) ? scope->lookupImportedType(petv.prefix->value, petv.name.value) : scope->lookupType(petv.name.value); if (!tf.has_value()) return true; auto [typeArguments, packArguments] = saturateArguments(solver->arena, solver->singletonTypes, *tf, petv.typeArguments, petv.packArguments); if (follow(tf->type) == follow(signature.fn.type) && (signature.arguments != typeArguments || signature.packArguments != packArguments)) { foundInfiniteType = true; return false; } return true; } }; struct InstantiationQueuer : TypeVarOnceVisitor { ConstraintSolver* solver; const InstantiationSignature& signature; NotNull scope; Location location; explicit InstantiationQueuer(NotNull scope, const Location& location, ConstraintSolver* solver, const InstantiationSignature& signature) : solver(solver) , signature(signature) , scope(scope) , location(location) { } bool visit(TypeId ty, const PendingExpansionTypeVar& petv) override { solver->pushConstraint(scope, location, TypeAliasExpansionConstraint{ty}); return false; } }; bool ConstraintSolver::tryDispatch(const TypeAliasExpansionConstraint& c, NotNull constraint) { const PendingExpansionTypeVar* petv = get(follow(c.target)); if (!petv) { unblock(c.target); return true; } auto bindResult = [this, &c](TypeId result) { asMutable(c.target)->ty.emplace(result); unblock(c.target); }; std::optional tf = (petv->prefix) ? constraint->scope->lookupImportedType(petv->prefix->value, petv->name.value) : constraint->scope->lookupType(petv->name.value); if (!tf.has_value()) { reportError(UnknownSymbol{petv->name.value, UnknownSymbol::Context::Type}, constraint->location); bindResult(errorRecoveryType()); return true; } // If there are no parameters to the type function we can just use the type // directly. if (tf->typeParams.empty() && tf->typePackParams.empty()) { bindResult(tf->type); return true; } auto [typeArguments, packArguments] = saturateArguments(arena, singletonTypes, *tf, petv->typeArguments, petv->packArguments); bool sameTypes = std::equal(typeArguments.begin(), typeArguments.end(), tf->typeParams.begin(), tf->typeParams.end(), [](auto&& itp, auto&& p) { return itp == p.ty; }); bool samePacks = std::equal(packArguments.begin(), packArguments.end(), tf->typePackParams.begin(), tf->typePackParams.end(), [](auto&& itp, auto&& p) { return itp == p.tp; }); // If we're instantiating the type with its generic saturatedTypeArguments we are // performing the identity substitution. We can just short-circuit and bind // to the TypeFun's type. if (sameTypes && samePacks) { bindResult(tf->type); return true; } InstantiationSignature signature{ *tf, typeArguments, packArguments, }; // If we use the same signature, we don't need to bother trying to // instantiate the alias again, since the instantiation should be // deterministic. if (TypeId* cached = instantiatedAliases.find(signature)) { bindResult(*cached); return true; } // In order to prevent infinite types from being expanded and causing us to // cycle infinitely, we need to scan the type function for cases where we // expand the same alias with different type saturatedTypeArguments. See // https://github.com/Roblox/luau/pull/68 for the RFC responsible for this. // This is a little nicer than using a recursion limit because we can catch // the infinite expansion before actually trying to expand it. InfiniteTypeFinder itf{this, signature, constraint->scope}; itf.traverse(tf->type); if (itf.foundInfiniteType) { // TODO (CLI-56761): Report an error. bindResult(errorRecoveryType()); return true; } ApplyTypeFunction applyTypeFunction{arena}; for (size_t i = 0; i < typeArguments.size(); ++i) { applyTypeFunction.typeArguments[tf->typeParams[i].ty] = typeArguments[i]; } for (size_t i = 0; i < packArguments.size(); ++i) { applyTypeFunction.typePackArguments[tf->typePackParams[i].tp] = packArguments[i]; } std::optional maybeInstantiated = applyTypeFunction.substitute(tf->type); // Note that ApplyTypeFunction::encounteredForwardedType is never set in // DCR, because we do not use free types for forward-declared generic // aliases. if (!maybeInstantiated.has_value()) { // TODO (CLI-56761): Report an error. bindResult(errorRecoveryType()); return true; } TypeId instantiated = *maybeInstantiated; TypeId target = follow(instantiated); if (target->persistent) return true; // Type function application will happily give us the exact same type if // there are e.g. generic saturatedTypeArguments that go unused. bool needsClone = follow(tf->type) == target; // Only tables have the properties we're trying to set. TableTypeVar* ttv = getMutableTableType(target); if (ttv) { if (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(target); MetatableTypeVar* mtv = getMutable(instantiated); mtv->table = applyTypeFunction.clone(mtv->table); ttv = getMutable(mtv->table); } else if (get(target)) { instantiated = applyTypeFunction.clone(target); ttv = getMutable(instantiated); } target = follow(instantiated); } ttv->instantiatedTypeParams = typeArguments; ttv->instantiatedTypePackParams = packArguments; // TODO: Fill in definitionModuleName. } bindResult(target); // The application is not recursive, so we need to queue up application of // any child type function instantiations within the result in order for it // to be complete. InstantiationQueuer queuer{constraint->scope, constraint->location, this, signature}; queuer.traverse(target); instantiatedAliases[signature] = target; return true; } bool ConstraintSolver::tryDispatch(const FunctionCallConstraint& c, NotNull constraint) { TypeId fn = follow(c.fn); TypePackId result = follow(c.result); if (isBlocked(c.fn)) { return block(c.fn, constraint); } // We don't support magic __call metamethods. if (std::optional callMm = findMetatableEntry(singletonTypes, errors, fn, "__call", constraint->location)) { std::vector args{fn}; for (TypeId arg : c.argsPack) args.push_back(arg); TypeId instantiatedType = arena->addType(BlockedTypeVar{}); TypeId inferredFnType = arena->addType(FunctionTypeVar(TypeLevel{}, constraint->scope.get(), arena->addTypePack(TypePack{args, {}}), c.result)); // Alter the inner constraints. LUAU_ASSERT(c.innerConstraints.size() == 2); // Anything that is blocked on this constraint must also be blocked on our inner constraints auto blockedIt = blocked.find(constraint.get()); if (blockedIt != blocked.end()) { for (const auto& ic : c.innerConstraints) { for (const auto& blockedConstraint : blockedIt->second) block(ic, blockedConstraint); } } asMutable(*c.innerConstraints.at(0)).c = InstantiationConstraint{instantiatedType, *callMm}; asMutable(*c.innerConstraints.at(1)).c = SubtypeConstraint{inferredFnType, instantiatedType}; unsolvedConstraints.insert(end(unsolvedConstraints), begin(c.innerConstraints), end(c.innerConstraints)); asMutable(c.result)->ty.emplace(constraint->scope); unblock(c.result); return true; } const FunctionTypeVar* ftv = get(fn); bool usedMagic = false; if (ftv && ftv->dcrMagicFunction != nullptr) { usedMagic = ftv->dcrMagicFunction(MagicFunctionCallContext{NotNull(this), c.callSite, c.argsPack, result}); } if (usedMagic) { // There are constraints that are blocked on these constraints. If we // are never going to even examine them, then we should not block // anything else on them. // // TODO CLI-58842 #if 0 for (auto& c: c.innerConstraints) unblock(c); #endif } else { // Anything that is blocked on this constraint must also be blocked on our inner constraints auto blockedIt = blocked.find(constraint.get()); if (blockedIt != blocked.end()) { for (const auto& ic : c.innerConstraints) { for (const auto& blockedConstraint : blockedIt->second) block(ic, blockedConstraint); } } unsolvedConstraints.insert(end(unsolvedConstraints), begin(c.innerConstraints), end(c.innerConstraints)); asMutable(c.result)->ty.emplace(constraint->scope); } unblock(c.result); return true; } bool ConstraintSolver::tryDispatch(const PrimitiveTypeConstraint& c, NotNull constraint) { TypeId expectedType = follow(c.expectedType); if (isBlocked(expectedType) || get(expectedType)) return block(expectedType, constraint); TypeId bindTo = maybeSingleton(expectedType) ? c.singletonType : c.multitonType; asMutable(c.resultType)->ty.emplace(bindTo); return true; } bool ConstraintSolver::tryDispatch(const HasPropConstraint& c, NotNull constraint) { TypeId subjectType = follow(c.subjectType); if (isBlocked(subjectType) || get(subjectType)) return block(subjectType, constraint); std::optional resultType = lookupTableProp(subjectType, c.prop); if (!resultType) return false; if (isBlocked(*resultType)) { block(*resultType, constraint); return false; } asMutable(c.resultType)->ty.emplace(*resultType); return true; } static bool isUnsealedTable(TypeId ty) { ty = follow(ty); const TableTypeVar* ttv = get(ty); return ttv && ttv->state == TableState::Unsealed; } /** * Create a shallow copy of `ty` and its properties along `path`. Insert a new * property (the last segment of `path`) into the tail table with the value `t`. * * On success, returns the new outermost table type. If the root table or any * of its subkeys are not unsealed tables, the function fails and returns * std::nullopt. * * TODO: Prove that we completely give up in the face of indexers and * metatables. */ static std::optional updateTheTableType(NotNull arena, TypeId ty, const std::vector& path, TypeId replaceTy) { if (path.empty()) return std::nullopt; // First walk the path and ensure that it's unsealed tables all the way // to the end. { TypeId t = ty; for (size_t i = 0; i < path.size() - 1; ++i) { if (!isUnsealedTable(t)) return std::nullopt; const TableTypeVar* tbl = get(t); auto it = tbl->props.find(path[i]); if (it == tbl->props.end()) return std::nullopt; t = it->second.type; } // The last path segment should not be a property of the table at all. // We are not changing property types. We are only admitting this one // new property to be appended. if (!isUnsealedTable(t)) return std::nullopt; const TableTypeVar* tbl = get(t); if (0 != tbl->props.count(path.back())) return std::nullopt; } const TypeId res = shallowClone(ty, arena); TypeId t = res; for (size_t i = 0; i < path.size() - 1; ++i) { const std::string segment = path[i]; TableTypeVar* ttv = getMutable(t); LUAU_ASSERT(ttv); auto propIt = ttv->props.find(segment); if (propIt != ttv->props.end()) { LUAU_ASSERT(isUnsealedTable(propIt->second.type)); t = shallowClone(follow(propIt->second.type), arena); ttv->props[segment].type = t; } else return std::nullopt; } TableTypeVar* ttv = getMutable(t); LUAU_ASSERT(ttv); const std::string lastSegment = path.back(); LUAU_ASSERT(0 == ttv->props.count(lastSegment)); ttv->props[lastSegment] = Property{replaceTy}; return res; } bool ConstraintSolver::tryDispatch(const SetPropConstraint& c, NotNull constraint) { TypeId subjectType = follow(c.subjectType); if (isBlocked(subjectType)) return block(subjectType, constraint); std::optional existingPropType = subjectType; for (const std::string& segment : c.path) { ErrorVec e; std::optional propTy = lookupTableProp(*existingPropType, segment); if (!propTy) { existingPropType = std::nullopt; break; } else if (isBlocked(*propTy)) return block(*propTy, constraint); else existingPropType = follow(*propTy); } auto bind = [](TypeId a, TypeId b) { asMutable(a)->ty.emplace(b); }; if (existingPropType) { unify(c.propType, *existingPropType, constraint->scope); bind(c.resultType, c.subjectType); return true; } if (get(subjectType)) { TypeId ty = arena->freshType(constraint->scope); // Mint a chain of free tables per c.path for (auto it = rbegin(c.path); it != rend(c.path); ++it) { TableTypeVar t{TableState::Free, TypeLevel{}, constraint->scope}; t.props[*it] = {ty}; ty = arena->addType(std::move(t)); } LUAU_ASSERT(ty); bind(subjectType, ty); bind(c.resultType, ty); return true; } else if (auto ttv = getMutable(subjectType)) { if (ttv->state == TableState::Free) { ttv->props[c.path[0]] = Property{c.propType}; bind(c.resultType, c.subjectType); return true; } else if (ttv->state == TableState::Unsealed) { std::optional augmented = updateTheTableType(NotNull{arena}, subjectType, c.path, c.propType); bind(c.resultType, augmented.value_or(subjectType)); return true; } else { bind(c.resultType, subjectType); return true; } } else if (get(subjectType) || get(subjectType)) { bind(c.resultType, subjectType); return true; } LUAU_ASSERT(0); return true; } bool ConstraintSolver::tryDispatch(const SingletonOrTopTypeConstraint& c, NotNull constraint) { if (isBlocked(c.discriminantType)) return false; TypeId followed = follow(c.discriminantType); // `nil` is a singleton type too! There's only one value of type `nil`. if (get(followed) || isNil(followed)) *asMutable(c.resultType) = NegationTypeVar{c.discriminantType}; else *asMutable(c.resultType) = BoundTypeVar{singletonTypes->unknownType}; return true; } bool ConstraintSolver::tryDispatchIterableTable(TypeId iteratorTy, const IterableConstraint& c, NotNull constraint, bool force) { auto block_ = [&](auto&& t) { if (force) { // TODO: I believe it is the case that, if we are asked to force // this constraint, then we can do nothing but fail. I'd like to // find a code sample that gets here. LUAU_ASSERT(false); } else block(t, constraint); return false; }; // We may have to block here if we don't know what the iteratee type is, // if it's a free table, if we don't know it has a metatable, and so on. iteratorTy = follow(iteratorTy); if (get(iteratorTy)) return block_(iteratorTy); auto anyify = [&](auto ty) { Anyification anyify{arena, constraint->scope, singletonTypes, &iceReporter, singletonTypes->anyType, singletonTypes->anyTypePack}; std::optional anyified = anyify.substitute(ty); if (!anyified) reportError(CodeTooComplex{}, constraint->location); else unify(*anyified, ty, constraint->scope); }; auto errorify = [&](auto ty) { Anyification anyify{arena, constraint->scope, singletonTypes, &iceReporter, errorRecoveryType(), errorRecoveryTypePack()}; std::optional errorified = anyify.substitute(ty); if (!errorified) reportError(CodeTooComplex{}, constraint->location); else unify(*errorified, ty, constraint->scope); }; if (get(iteratorTy)) { anyify(c.variables); return true; } if (get(iteratorTy)) { errorify(c.variables); return true; } // Irksome: I don't think we have any way to guarantee that this table // type never has a metatable. if (auto iteratorTable = get(iteratorTy)) { if (iteratorTable->state == TableState::Free) return block_(iteratorTy); if (iteratorTable->indexer) { TypePackId expectedVariablePack = arena->addTypePack({iteratorTable->indexer->indexType, iteratorTable->indexer->indexResultType}); unify(c.variables, expectedVariablePack, constraint->scope); } else errorify(c.variables); } else if (std::optional iterFn = findMetatableEntry(singletonTypes, errors, iteratorTy, "__iter", Location{})) { if (isBlocked(*iterFn)) { return block(*iterFn, constraint); } Instantiation instantiation(TxnLog::empty(), arena, TypeLevel{}, constraint->scope); if (std::optional instantiatedIterFn = instantiation.substitute(*iterFn)) { if (auto iterFtv = get(*instantiatedIterFn)) { TypePackId expectedIterArgs = arena->addTypePack({iteratorTy}); unify(iterFtv->argTypes, expectedIterArgs, constraint->scope); std::vector iterRets = flatten(*arena, singletonTypes, iterFtv->retTypes, 2); if (iterRets.size() < 1) { // We've done what we can; this will get reported as an // error by the type checker. return true; } TypeId nextFn = iterRets[0]; TypeId table = iterRets.size() == 2 ? iterRets[1] : arena->freshType(constraint->scope); if (std::optional instantiatedNextFn = instantiation.substitute(nextFn)) { const TypeId firstIndex = arena->freshType(constraint->scope); // nextTy : (iteratorTy, indexTy?) -> (indexTy, valueTailTy...) const TypePackId nextArgPack = arena->addTypePack({table, arena->addType(UnionTypeVar{{firstIndex, singletonTypes->nilType}})}); const TypePackId valueTailTy = arena->addTypePack(FreeTypePack{constraint->scope}); const TypePackId nextRetPack = arena->addTypePack(TypePack{{firstIndex}, valueTailTy}); const TypeId expectedNextTy = arena->addType(FunctionTypeVar{nextArgPack, nextRetPack}); unify(*instantiatedNextFn, expectedNextTy, constraint->scope); pushConstraint(constraint->scope, constraint->location, PackSubtypeConstraint{c.variables, nextRetPack}); } else { reportError(UnificationTooComplex{}, constraint->location); } } else { // TODO: Support __call and function overloads (what does an overload even mean for this?) } } else { reportError(UnificationTooComplex{}, constraint->location); } } else if (auto iteratorMetatable = get(iteratorTy)) { TypeId metaTy = follow(iteratorMetatable->metatable); if (get(metaTy)) return block_(metaTy); LUAU_ASSERT(false); } else errorify(c.variables); return true; } bool ConstraintSolver::tryDispatchIterableFunction( TypeId nextTy, TypeId tableTy, TypeId firstIndexTy, const IterableConstraint& c, NotNull constraint, bool force) { // We need to know whether or not this type is nil or not. // If we don't know, block and reschedule ourselves. firstIndexTy = follow(firstIndexTy); if (get(firstIndexTy)) { if (force) LUAU_ASSERT(false); else block(firstIndexTy, constraint); return false; } const TypeId firstIndex = isNil(firstIndexTy) ? arena->freshType(constraint->scope) // FIXME: Surely this should be a union (free | nil) : firstIndexTy; // nextTy : (tableTy, indexTy?) -> (indexTy, valueTailTy...) const TypePackId nextArgPack = arena->addTypePack({tableTy, arena->addType(UnionTypeVar{{firstIndex, singletonTypes->nilType}})}); const TypePackId valueTailTy = arena->addTypePack(FreeTypePack{constraint->scope}); const TypePackId nextRetPack = arena->addTypePack(TypePack{{firstIndex}, valueTailTy}); const TypeId expectedNextTy = arena->addType(FunctionTypeVar{TypeLevel{}, constraint->scope, nextArgPack, nextRetPack}); unify(nextTy, expectedNextTy, constraint->scope); pushConstraint(constraint->scope, constraint->location, PackSubtypeConstraint{c.variables, nextRetPack}); return true; } std::optional ConstraintSolver::lookupTableProp(TypeId subjectType, const std::string& propName) { auto collectParts = [&](auto&& unionOrIntersection) -> std::pair, std::vector> { std::optional blocked; std::vector parts; for (TypeId expectedPart : unionOrIntersection) { expectedPart = follow(expectedPart); if (isBlocked(expectedPart) || get(expectedPart)) blocked = expectedPart; else if (const TableTypeVar* ttv = get(follow(expectedPart))) { if (auto prop = ttv->props.find(propName); prop != ttv->props.end()) parts.push_back(prop->second.type); else if (ttv->indexer && maybeString(ttv->indexer->indexType)) parts.push_back(ttv->indexer->indexResultType); } } return {blocked, parts}; }; std::optional resultType; if (auto ttv = get(subjectType)) { if (auto prop = ttv->props.find(propName); prop != ttv->props.end()) resultType = prop->second.type; else if (ttv->indexer && maybeString(ttv->indexer->indexType)) resultType = ttv->indexer->indexResultType; } else if (auto utv = get(subjectType)) { auto [blocked, parts] = collectParts(utv); if (blocked) resultType = *blocked; else if (parts.size() == 1) resultType = parts[0]; else if (parts.size() > 1) resultType = arena->addType(UnionTypeVar{std::move(parts)}); else LUAU_ASSERT(false); // parts.size() == 0 } else if (auto itv = get(subjectType)) { auto [blocked, parts] = collectParts(itv); if (blocked) resultType = *blocked; else if (parts.size() == 1) resultType = parts[0]; else if (parts.size() > 1) resultType = arena->addType(IntersectionTypeVar{std::move(parts)}); else LUAU_ASSERT(false); // parts.size() == 0 } return resultType; } void ConstraintSolver::block_(BlockedConstraintId target, NotNull constraint) { blocked[target].push_back(constraint); auto& count = blockedConstraints[constraint]; count += 1; } void ConstraintSolver::block(NotNull target, NotNull constraint) { if (FFlag::DebugLuauLogSolverToJson) logger->pushBlock(constraint, target); if (FFlag::DebugLuauLogSolver) printf("block Constraint %s on\t%s\n", toString(*target, opts).c_str(), toString(*constraint, opts).c_str()); block_(target, constraint); } bool ConstraintSolver::block(TypeId target, NotNull constraint) { if (FFlag::DebugLuauLogSolverToJson) logger->pushBlock(constraint, target); if (FFlag::DebugLuauLogSolver) printf("block TypeId %s on\t%s\n", toString(target, opts).c_str(), toString(*constraint, opts).c_str()); block_(target, constraint); return false; } bool ConstraintSolver::block(TypePackId target, NotNull constraint) { if (FFlag::DebugLuauLogSolverToJson) logger->pushBlock(constraint, target); if (FFlag::DebugLuauLogSolver) printf("block TypeId %s on\t%s\n", toString(target, opts).c_str(), toString(*constraint, opts).c_str()); block_(target, constraint); return false; } struct Blocker : TypeVarOnceVisitor { NotNull solver; NotNull constraint; bool blocked = false; explicit Blocker(NotNull solver, NotNull constraint) : solver(solver) , constraint(constraint) { } bool visit(TypeId ty, const BlockedTypeVar&) { blocked = true; solver->block(ty, constraint); return false; } bool visit(TypeId ty, const PendingExpansionTypeVar&) { blocked = true; solver->block(ty, constraint); return false; } }; bool ConstraintSolver::recursiveBlock(TypeId target, NotNull constraint) { Blocker blocker{NotNull{this}, constraint}; blocker.traverse(target); return !blocker.blocked; } bool ConstraintSolver::recursiveBlock(TypePackId pack, NotNull constraint) { Blocker blocker{NotNull{this}, constraint}; blocker.traverse(pack); return !blocker.blocked; } void ConstraintSolver::unblock_(BlockedConstraintId progressed) { auto it = blocked.find(progressed); if (it == blocked.end()) return; // unblocked should contain a value always, because of the above check for (NotNull unblockedConstraint : it->second) { auto& count = blockedConstraints[unblockedConstraint]; if (FFlag::DebugLuauLogSolver) printf("Unblocking count=%d\t%s\n", int(count), toString(*unblockedConstraint, opts).c_str()); // This assertion being hit indicates that `blocked` and // `blockedConstraints` desynchronized at some point. This is problematic // because we rely on this count being correct to skip over blocked // constraints. LUAU_ASSERT(count > 0); count -= 1; } blocked.erase(it); } void ConstraintSolver::unblock(NotNull progressed) { if (FFlag::DebugLuauLogSolverToJson) logger->popBlock(progressed); return unblock_(progressed); } void ConstraintSolver::unblock(TypeId progressed) { if (FFlag::DebugLuauLogSolverToJson) logger->popBlock(progressed); return unblock_(progressed); } void ConstraintSolver::unblock(TypePackId progressed) { if (FFlag::DebugLuauLogSolverToJson) logger->popBlock(progressed); return unblock_(progressed); } void ConstraintSolver::unblock(const std::vector& types) { for (TypeId t : types) unblock(t); } void ConstraintSolver::unblock(const std::vector& packs) { for (TypePackId t : packs) unblock(t); } bool ConstraintSolver::isBlocked(TypeId ty) { return nullptr != get(follow(ty)) || nullptr != get(follow(ty)); } bool ConstraintSolver::isBlocked(TypePackId tp) { return nullptr != get(follow(tp)); } bool ConstraintSolver::isBlocked(NotNull constraint) { auto blockedIt = blockedConstraints.find(constraint); return blockedIt != blockedConstraints.end() && blockedIt->second > 0; } void ConstraintSolver::unify(TypeId subType, TypeId superType, NotNull scope) { Unifier u{normalizer, Mode::Strict, scope, Location{}, Covariant}; u.useScopes = true; u.tryUnify(subType, superType); if (!u.errors.empty()) { TypeId errorType = errorRecoveryType(); u.tryUnify(subType, errorType); u.tryUnify(superType, errorType); } const auto [changedTypes, changedPacks] = u.log.getChanges(); u.log.commit(); unblock(changedTypes); unblock(changedPacks); } void ConstraintSolver::unify(TypePackId subPack, TypePackId superPack, NotNull scope) { UnifierSharedState sharedState{&iceReporter}; Unifier u{normalizer, Mode::Strict, scope, Location{}, Covariant}; u.useScopes = true; u.tryUnify(subPack, superPack); const auto [changedTypes, changedPacks] = u.log.getChanges(); u.log.commit(); unblock(changedTypes); unblock(changedPacks); } void ConstraintSolver::pushConstraint(NotNull scope, const Location& location, ConstraintV cv) { std::unique_ptr c = std::make_unique(scope, location, std::move(cv)); NotNull borrow = NotNull(c.get()); solverConstraints.push_back(std::move(c)); unsolvedConstraints.push_back(borrow); } TypeId ConstraintSolver::resolveModule(const ModuleInfo& info, const Location& location) { if (info.name.empty()) { reportError(UnknownRequire{}, location); return errorRecoveryType(); } std::string humanReadableName = moduleResolver->getHumanReadableModuleName(info.name); for (const auto& [location, path] : requireCycles) { if (!path.empty() && path.front() == humanReadableName) return singletonTypes->anyType; } ModulePtr module = moduleResolver->getModule(info.name); if (!module) { if (!moduleResolver->moduleExists(info.name) && !info.optional) reportError(UnknownRequire{humanReadableName}, location); return errorRecoveryType(); } if (module->type != SourceCode::Type::Module) { reportError(IllegalRequire{humanReadableName, "Module is not a ModuleScript. It cannot be required."}, location); return errorRecoveryType(); } TypePackId modulePack = module->getModuleScope()->returnType; if (get(modulePack)) return errorRecoveryType(); std::optional moduleType = first(modulePack); if (!moduleType) { reportError(IllegalRequire{humanReadableName, "Module does not return exactly 1 value. It cannot be required."}, location); return errorRecoveryType(); } return *moduleType; } void ConstraintSolver::reportError(TypeErrorData&& data, const Location& location) { errors.emplace_back(location, std::move(data)); errors.back().moduleName = currentModuleName; } void ConstraintSolver::reportError(TypeError e) { errors.emplace_back(std::move(e)); errors.back().moduleName = currentModuleName; } TypeId ConstraintSolver::errorRecoveryType() const { return singletonTypes->errorRecoveryType(); } TypePackId ConstraintSolver::errorRecoveryTypePack() const { return singletonTypes->errorRecoveryTypePack(); } TypeId ConstraintSolver::unionOfTypes(TypeId a, TypeId b, NotNull scope, bool unifyFreeTypes) { a = follow(a); b = follow(b); if (unifyFreeTypes && (get(a) || get(b))) { Unifier u{normalizer, Mode::Strict, scope, Location{}, Covariant}; u.useScopes = true; u.tryUnify(b, a); if (u.errors.empty()) { u.log.commit(); return a; } else { return singletonTypes->errorRecoveryType(singletonTypes->anyType); } } if (*a == *b) return a; std::vector types = reduceUnion({a, b}); if (types.empty()) return singletonTypes->neverType; if (types.size() == 1) return types[0]; return arena->addType(UnionTypeVar{types}); } } // namespace Luau