// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details #include "Luau/Subtyping.h" #include "Luau/Common.h" #include "Luau/Normalize.h" #include "Luau/Type.h" #include "Luau/TypePack.h" #include "Luau/TypeUtils.h" #include namespace Luau { SubtypingGraph SubtypingGraph::and_(const SubtypingGraph& other) { return SubtypingGraph{ isSubtype && other.isSubtype, // `||` is intentional here, we want to preserve error-suppressing flag. isErrorSuppressing || other.isErrorSuppressing, normalizationTooComplex || other.normalizationTooComplex, }; } SubtypingGraph SubtypingGraph::or_(const SubtypingGraph& other) { return SubtypingGraph{ isSubtype || other.isSubtype, isErrorSuppressing || other.isErrorSuppressing, normalizationTooComplex || other.normalizationTooComplex, }; } SubtypingGraph SubtypingGraph::and_(const std::vector& results) { SubtypingGraph acc{true, false}; for (const SubtypingGraph& current : results) acc = acc.and_(current); return acc; } SubtypingGraph SubtypingGraph::or_(const std::vector& results) { SubtypingGraph acc{false, false}; for (const SubtypingGraph& current : results) acc = acc.or_(current); return acc; } SubtypingGraph Subtyping::isSubtype(TypeId subTy, TypeId superTy) { subTy = follow(subTy); superTy = follow(superTy); // TODO: Do we care about returning a proof that this is error-suppressing? // e.g. given `a | error <: a | error` where both operands are pointer equal, // then should it also carry the information that it's error-suppressing? // If it should, then `error <: error` should also do the same. if (subTy == superTy) return {true}; if (auto superUnion = get(superTy)) return isSubtype(subTy, superUnion); else if (auto subUnion = get(subTy)) return isSubtype(subUnion, superTy); else if (auto superIntersection = get(superTy)) return isSubtype(subTy, superIntersection); else if (auto subIntersection = get(subTy)) { SubtypingGraph result = isSubtype(subIntersection, superTy); if (result.isSubtype || result.isErrorSuppressing || result.normalizationTooComplex) return result; else return isSubtype(normalizer->normalize(subTy), normalizer->normalize(superTy)); } else if (get(superTy)) return {true}; // This is always true. else if (get(subTy)) { // any = unknown | error, so we rewrite this to match. // As per TAPL: A | B <: T iff A <: T && B <: T return isSubtype(builtinTypes->unknownType, superTy).and_(isSubtype(builtinTypes->errorType, superTy)); } else if (auto superUnknown = get(superTy)) { LUAU_ASSERT(!get(subTy)); // TODO: replace with ice. LUAU_ASSERT(!get(subTy)); // TODO: replace with ice. LUAU_ASSERT(!get(subTy)); // TODO: replace with ice. bool errorSuppressing = get(subTy); return {!errorSuppressing, errorSuppressing}; } else if (get(subTy)) return {true}; else if (get(superTy)) return {false, true}; else if (get(subTy)) return {false, true}; else if (auto p = get2(subTy, superTy)) return isSubtype(p); else if (auto p = get2(subTy, superTy)) return isSubtype(p); else if (auto p = get2(subTy, superTy)) return isSubtype(p); else if (auto p = get2(subTy, superTy)) return isSubtype(p); return {false}; } SubtypingGraph Subtyping::isSubtype(TypePackId subTp, TypePackId superTp) { subTp = follow(subTp); superTp = follow(superTp); auto [subHead, subTail] = flatten(subTp); auto [superHead, superTail] = flatten(superTp); const size_t headSize = std::min(subHead.size(), superHead.size()); std::vector results; results.reserve(std::max(subHead.size(), superHead.size()) + 1); // Match head types pairwise for (size_t i = 0; i < headSize; ++i) { results.push_back(isSubtype(subHead[i], superHead[i])); if (!results.back().isSubtype) return {false}; } // Handle mismatched head sizes if (subHead.size() < superHead.size()) { if (subTail) { if (auto vt = get(*subTail)) { for (size_t i = headSize; i < superHead.size(); ++i) { results.push_back(isSubtype(vt->ty, superHead[i])); } } else LUAU_ASSERT(0); // TODO } else return {false}; } else if (subHead.size() > superHead.size()) { if (superTail) { if (auto vt = get(*superTail)) { for (size_t i = headSize; i < subHead.size(); ++i) { results.push_back(isSubtype(subHead[i], vt->ty)); } } else LUAU_ASSERT(0); // TODO } else return {false}; } else { // subHead and superHead are the same size. Nothing more must be done. } // Handle tails if (subTail && superTail) { if (auto p = get2(*subTail, *superTail)) { results.push_back(isSubtype(p.first->ty, p.second->ty)); } else LUAU_ASSERT(0); // TODO } else if (subTail) { if (get(*subTail)) { return {false}; } LUAU_ASSERT(0); // TODO } else if (superTail) { if (get(*superTail)) { /* * A variadic type pack ...T can be thought of as an infinite union of finite type packs. * () | (T) | (T, T) | (T, T, T) | ... * * And, per TAPL: * T <: A | B iff T <: A or T <: B * * All variadic type packs are therefore supertypes of the empty type pack. */ } else LUAU_ASSERT(0); // TODO } return SubtypingGraph::and_(results); } template SubtypingGraph Subtyping::isSubtype(const TryPair& pair) { return isSubtype(pair.first, pair.second); } /* * This is much simpler than the Unifier implementation because we don't * actually care about potential "cross-talk" between union parts that match the * left side. * * In fact, we're very limited in what we can do: If multiple choices match, but * all of them have non-overlapping constraints, then we're stuck with an "or" * conjunction of constraints. Solving this in the general case is quite * difficult. * * For example, we cannot dispatch anything from this constraint: * * {x: number, y: string} <: {x: number, y: 'a} | {x: 'b, y: string} * * From this constraint, we can know that either string <: 'a or number <: 'b, * but we don't know which! * * However: * * {x: number, y: string} <: {x: number, y: 'a} | {x: number, y: string} * * We can dispatch this constraint because there is no 'or' conjunction. One of * the arms requires 0 matches. * * {x: number, y: string, z: boolean} | {x: number, y: 'a, z: 'b} | {x: number, * y: string, z: 'b} * * Here, we have two matches. One asks for string ~ 'a and boolean ~ 'b. The * other just asks for boolean ~ 'b. We can dispatch this and only commit * boolean ~ 'b. This constraint does not teach us anything about 'a. */ SubtypingGraph Subtyping::isSubtype(TypeId subTy, const UnionType* superUnion) { // As per TAPL: T <: A | B iff T <: A || T <: B std::vector subtypings; for (TypeId ty : superUnion) subtypings.push_back(isSubtype(subTy, ty)); return SubtypingGraph::or_(subtypings); } SubtypingGraph Subtyping::isSubtype(const UnionType* subUnion, TypeId superTy) { // As per TAPL: A | B <: T iff A <: T && B <: T std::vector subtypings; for (TypeId ty : subUnion) subtypings.push_back(isSubtype(ty, superTy)); return SubtypingGraph::and_(subtypings); } SubtypingGraph Subtyping::isSubtype(TypeId subTy, const IntersectionType* superIntersection) { // As per TAPL: T <: A & B iff T <: A && T <: B std::vector subtypings; for (TypeId ty : superIntersection) subtypings.push_back(isSubtype(subTy, ty)); return SubtypingGraph::and_(subtypings); } SubtypingGraph Subtyping::isSubtype(const IntersectionType* subIntersection, TypeId superTy) { // TODO: Semantic subtyping here. // As per TAPL: A & B <: T iff A <: T || B <: T std::vector subtypings; for (TypeId ty : subIntersection) subtypings.push_back(isSubtype(ty, superTy)); return SubtypingGraph::or_(subtypings); } SubtypingGraph Subtyping::isSubtype(const PrimitiveType* subPrim, const PrimitiveType* superPrim) { return {subPrim->type == superPrim->type}; } SubtypingGraph Subtyping::isSubtype(const SingletonType* subSingleton, const PrimitiveType* superPrim) { if (get(subSingleton) && superPrim->type == PrimitiveType::String) return {true}; else if (get(subSingleton) && superPrim->type == PrimitiveType::Boolean) return {true}; else return {false}; } SubtypingGraph Subtyping::isSubtype(const SingletonType* subSingleton, const SingletonType* superSingleton) { return {*subSingleton == *superSingleton}; } SubtypingGraph Subtyping::isSubtype(const FunctionType* subFunction, const FunctionType* superFunction) { SubtypingGraph argResult = isSubtype(superFunction->argTypes, subFunction->argTypes); SubtypingGraph retResult = isSubtype(subFunction->retTypes, superFunction->retTypes); return argResult.and_(retResult); } SubtypingGraph Subtyping::isSubtype(const NormalizedType* subNorm, const NormalizedType* superNorm) { if (!subNorm || !superNorm) return {false, true, true}; SubtypingGraph result{true}; result = result.and_(isSubtype(subNorm->tops, superNorm->tops)); result = result.and_(isSubtype(subNorm->booleans, superNorm->booleans)); // isSubtype(subNorm->classes, superNorm->classes); // isSubtype(subNorm->classes, superNorm->tables); result = result.and_(isSubtype(subNorm->errors, superNorm->errors)); result = result.and_(isSubtype(subNorm->nils, superNorm->nils)); result = result.and_(isSubtype(subNorm->numbers, superNorm->numbers)); result.isSubtype &= Luau::isSubtype(subNorm->strings, superNorm->strings); // isSubtype(subNorm->strings, superNorm->tables); result = result.and_(isSubtype(subNorm->threads, superNorm->threads)); // isSubtype(subNorm->tables, superNorm->tables); // isSubtype(subNorm->tables, superNorm->strings); // isSubtype(subNorm->tables, superNorm->classes); // isSubtype(subNorm->functions, superNorm->functions); // isSubtype(subNorm->tyvars, superNorm->tyvars); return result; } } // namespace Luau