luau/Analysis/src/Subtyping.cpp

345 lines
11 KiB
C++
Raw Normal View History

Sync to upstream/release/591 (#1012) * Fix a use-after-free bug in the new type cloning algorithm * Tighten up the type of `coroutine.wrap`. It is now `<A..., R...>(f: (A...) -> R...) -> ((A...) -> R...)` * Break `.luaurc` out into a separate library target `Luau.Config`. This makes it easier for applications to reason about config files without also depending on the type inference engine. * Move typechecking limits into `FrontendOptions`. This allows embedders more finely-grained control over autocomplete's internal time limits. * Fix stability issue with debugger onprotectederror callback allowing break in non-yieldable contexts New solver: * Initial work toward [Local Type Inference](https://github.com/Roblox/luau/blob/0e1082108fd6fb3a32dfdf5f1766ea3fc1391328/rfcs/local-type-inference.md) * Introduce a new subtyping test. This will be much nicer than the old test because it is completely separate both from actual type inference and from error reporting. Native code generation: * Added function to compute iterated dominance frontier * Optimize barriers in SET_UPVALUE when tag is known * Cache lua_State::global in a register on A64 * Optimize constant stores in A64 lowering * Track table array size state to optimize array size checks * Add split tag/value store into a VM register * Check that spills can outlive the block only in specific conditions --------- Co-authored-by: Arseny Kapoulkine <arseny.kapoulkine@gmail.com> Co-authored-by: Vyacheslav Egorov <vegorov@roblox.com>
2023-08-18 19:15:41 +01:00
// 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 <algorithm>
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<SubtypingGraph>& results)
{
SubtypingGraph acc{true, false};
for (const SubtypingGraph& current : results)
acc = acc.and_(current);
return acc;
}
SubtypingGraph SubtypingGraph::or_(const std::vector<SubtypingGraph>& 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<UnionType>(superTy))
return isSubtype(subTy, superUnion);
else if (auto subUnion = get<UnionType>(subTy))
return isSubtype(subUnion, superTy);
else if (auto superIntersection = get<IntersectionType>(superTy))
return isSubtype(subTy, superIntersection);
else if (auto subIntersection = get<IntersectionType>(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<AnyType>(superTy))
return {true}; // This is always true.
else if (get<AnyType>(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<UnknownType>(superTy))
{
LUAU_ASSERT(!get<AnyType>(subTy)); // TODO: replace with ice.
LUAU_ASSERT(!get<UnionType>(subTy)); // TODO: replace with ice.
LUAU_ASSERT(!get<IntersectionType>(subTy)); // TODO: replace with ice.
bool errorSuppressing = get<ErrorType>(subTy);
return {!errorSuppressing, errorSuppressing};
}
else if (get<NeverType>(subTy))
return {true};
else if (get<ErrorType>(superTy))
return {false, true};
else if (get<ErrorType>(subTy))
return {false, true};
else if (auto p = get2<PrimitiveType, PrimitiveType>(subTy, superTy))
return isSubtype(p);
else if (auto p = get2<SingletonType, PrimitiveType>(subTy, superTy))
return isSubtype(p);
else if (auto p = get2<SingletonType, SingletonType>(subTy, superTy))
return isSubtype(p);
else if (auto p = get2<FunctionType, FunctionType>(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<SubtypingGraph> 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<VariadicTypePack>(*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<VariadicTypePack>(*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<VariadicTypePack, VariadicTypePack>(*subTail, *superTail))
{
results.push_back(isSubtype(p.first->ty, p.second->ty));
}
else
LUAU_ASSERT(0); // TODO
}
else if (subTail)
{
if (get<VariadicTypePack>(*subTail))
{
return {false};
}
LUAU_ASSERT(0); // TODO
}
else if (superTail)
{
if (get<VariadicTypePack>(*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<typename SubTy, typename SuperTy>
SubtypingGraph Subtyping::isSubtype(const TryPair<const SubTy*, const SuperTy*>& 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<SubtypingGraph> 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<SubtypingGraph> 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<SubtypingGraph> 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<SubtypingGraph> 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<StringSingleton>(subSingleton) && superPrim->type == PrimitiveType::String)
return {true};
else if (get<BooleanSingleton>(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