Sync to upstream/release/535

This commit is contained in:
Arseny Kapoulkine 2022-07-07 18:05:31 -07:00
parent 8f040862b1
commit 4a95f2201e
63 changed files with 5097 additions and 619 deletions

View file

@ -63,6 +63,7 @@ private:
AstLocal* local = nullptr;
};
std::vector<AstNode*> findAncestryAtPositionForAutocomplete(const SourceModule& source, Position pos);
std::vector<AstNode*> findAstAncestryOfPosition(const SourceModule& source, Position pos);
AstNode* findNodeAtPosition(const SourceModule& source, Position pos);
AstExpr* findExprAtPosition(const SourceModule& source, Position pos);

View file

@ -153,7 +153,7 @@ struct TypeChecker
const ScopePtr& scope, const AstExprBinary& expr, TypeId lhsType, TypeId rhsType, const PredicateVec& predicates = {});
TypeId checkBinaryOperation(
const ScopePtr& scope, const AstExprBinary& expr, TypeId lhsType, TypeId rhsType, const PredicateVec& predicates = {});
WithPredicate<TypeId> checkExpr(const ScopePtr& scope, const AstExprBinary& expr);
WithPredicate<TypeId> checkExpr(const ScopePtr& scope, const AstExprBinary& expr, std::optional<TypeId> expectedType = std::nullopt);
WithPredicate<TypeId> checkExpr(const ScopePtr& scope, const AstExprTypeAssertion& expr);
WithPredicate<TypeId> checkExpr(const ScopePtr& scope, const AstExprError& expr);
WithPredicate<TypeId> checkExpr(const ScopePtr& scope, const AstExprIfElse& expr, std::optional<TypeId> expectedType = std::nullopt);
@ -180,8 +180,12 @@ struct TypeChecker
const ScopePtr& scope, Unifier& state, TypePackId paramPack, TypePackId argPack, const std::vector<Location>& argLocations);
WithPredicate<TypePackId> checkExprPack(const ScopePtr& scope, const AstExpr& expr);
WithPredicate<TypePackId> checkExprPack(const ScopePtr& scope, const AstExprCall& expr);
WithPredicate<TypePackId> checkExprPackHelper(const ScopePtr& scope, const AstExpr& expr);
WithPredicate<TypePackId> checkExprPackHelper(const ScopePtr& scope, const AstExprCall& expr);
std::vector<std::optional<TypeId>> getExpectedTypesForCall(const std::vector<TypeId>& overloads, size_t argumentCount, bool selfCall);
std::optional<WithPredicate<TypePackId>> checkCallOverload(const ScopePtr& scope, const AstExprCall& expr, TypeId fn, TypePackId retPack,
TypePackId argPack, TypePack* args, const std::vector<Location>* argLocations, const WithPredicate<TypePackId>& argListResult,
std::vector<TypeId>& overloadsThatMatchArgCount, std::vector<TypeId>& overloadsThatDont, std::vector<OverloadErrorEntry>& errors);
@ -236,10 +240,11 @@ struct TypeChecker
void unifyLowerBound(TypePackId subTy, TypePackId superTy, TypeLevel demotedLevel, const Location& location);
std::optional<TypeId> findMetatableEntry(TypeId type, std::string entry, const Location& location);
std::optional<TypeId> findTablePropertyRespectingMeta(TypeId lhsType, Name name, const Location& location);
std::optional<TypeId> findMetatableEntry(TypeId type, std::string entry, const Location& location, bool addErrors);
std::optional<TypeId> findTablePropertyRespectingMeta(TypeId lhsType, Name name, const Location& location, bool addErrors);
std::optional<TypeId> getIndexTypeFromType(const ScopePtr& scope, TypeId type, const Name& name, const Location& location, bool addErrors);
std::optional<TypeId> getIndexTypeFromTypeImpl(const ScopePtr& scope, TypeId type, const Name& name, const Location& location, bool addErrors);
// Reduces the union to its simplest possible shape.
// (A | B) | B | C yields A | B | C
@ -316,11 +321,12 @@ private:
TypeIdPredicate mkTruthyPredicate(bool sense);
// Returns nullopt if the predicate filters down the TypeId to 0 options.
std::optional<TypeId> filterMap(TypeId type, TypeIdPredicate predicate);
// TODO: Return TypeId only.
std::optional<TypeId> filterMapImpl(TypeId type, TypeIdPredicate predicate);
std::pair<std::optional<TypeId>, bool> filterMap(TypeId type, TypeIdPredicate predicate);
public:
std::optional<TypeId> pickTypesFromSense(TypeId type, bool sense);
std::pair<std::optional<TypeId>, bool> pickTypesFromSense(TypeId type, bool sense);
private:
TypeId unionOfTypes(TypeId a, TypeId b, const Location& location, bool unifyFreeTypes = true);
@ -345,6 +351,7 @@ private:
TypePackId freshTypePack(TypeLevel level);
TypeId resolveType(const ScopePtr& scope, const AstType& annotation);
TypeId resolveTypeWorker(const ScopePtr& scope, const AstType& annotation);
TypePackId resolveTypePack(const ScopePtr& scope, const AstTypeList& types);
TypePackId resolveTypePack(const ScopePtr& scope, const AstTypePack& annotation);
TypeId instantiateTypeFun(const ScopePtr& scope, const TypeFun& tf, const std::vector<TypeId>& typeParams,
@ -412,8 +419,12 @@ public:
const TypeId booleanType;
const TypeId threadType;
const TypeId anyType;
const TypeId unknownType;
const TypeId neverType;
const TypePackId anyTypePack;
const TypePackId neverTypePack;
const TypePackId uninhabitableTypePack;
private:
int checkRecursionCount = 0;

View file

@ -173,5 +173,6 @@ std::pair<std::vector<TypeId>, std::optional<TypePackId>> flatten(TypePackId tp,
bool isVariadic(TypePackId tp);
bool isVariadic(TypePackId tp, const TxnLog& log);
bool containsNever(TypePackId tp);
} // namespace Luau

View file

@ -460,10 +460,18 @@ struct LazyTypeVar
std::function<TypeId()> thunk;
};
struct UnknownTypeVar
{
};
struct NeverTypeVar
{
};
using ErrorTypeVar = Unifiable::Error;
using TypeVariant = Unifiable::Variant<TypeId, PrimitiveTypeVar, ConstrainedTypeVar, BlockedTypeVar, SingletonTypeVar, FunctionTypeVar, TableTypeVar,
MetatableTypeVar, ClassTypeVar, AnyTypeVar, UnionTypeVar, IntersectionTypeVar, LazyTypeVar>;
MetatableTypeVar, ClassTypeVar, AnyTypeVar, UnionTypeVar, IntersectionTypeVar, LazyTypeVar, UnknownTypeVar, NeverTypeVar>;
struct TypeVar final
{
@ -575,8 +583,12 @@ struct SingletonTypes
const TypeId trueType;
const TypeId falseType;
const TypeId anyType;
const TypeId unknownType;
const TypeId neverType;
const TypePackId anyTypePack;
const TypePackId neverTypePack;
const TypePackId uninhabitableTypePack;
SingletonTypes();
~SingletonTypes();
@ -632,12 +644,30 @@ T* getMutable(TypeId tv)
return get_if<T>(&asMutable(tv)->ty);
}
/* Traverses the UnionTypeVar yielding each TypeId.
* If the iterator encounters a nested UnionTypeVar, it will instead yield each TypeId within.
*
* Beware: the iterator does not currently filter for unique TypeIds. This may change in the future.
const std::vector<TypeId>& getTypes(const UnionTypeVar* utv);
const std::vector<TypeId>& getTypes(const IntersectionTypeVar* itv);
const std::vector<TypeId>& getTypes(const ConstrainedTypeVar* ctv);
template<typename T>
struct TypeIterator;
using UnionTypeVarIterator = TypeIterator<UnionTypeVar>;
UnionTypeVarIterator begin(const UnionTypeVar* utv);
UnionTypeVarIterator end(const UnionTypeVar* utv);
using IntersectionTypeVarIterator = TypeIterator<IntersectionTypeVar>;
IntersectionTypeVarIterator begin(const IntersectionTypeVar* itv);
IntersectionTypeVarIterator end(const IntersectionTypeVar* itv);
using ConstrainedTypeVarIterator = TypeIterator<ConstrainedTypeVar>;
ConstrainedTypeVarIterator begin(const ConstrainedTypeVar* ctv);
ConstrainedTypeVarIterator end(const ConstrainedTypeVar* ctv);
/* Traverses the type T yielding each TypeId.
* If the iterator encounters a nested type T, it will instead yield each TypeId within.
*/
struct UnionTypeVarIterator
template<typename T>
struct TypeIterator
{
using value_type = Luau::TypeId;
using pointer = value_type*;
@ -645,33 +675,116 @@ struct UnionTypeVarIterator
using difference_type = size_t;
using iterator_category = std::input_iterator_tag;
explicit UnionTypeVarIterator(const UnionTypeVar* utv);
explicit TypeIterator(const T* t)
{
LUAU_ASSERT(t);
UnionTypeVarIterator& operator++();
UnionTypeVarIterator operator++(int);
bool operator!=(const UnionTypeVarIterator& rhs);
bool operator==(const UnionTypeVarIterator& rhs);
const std::vector<TypeId>& types = getTypes(t);
if (!types.empty())
stack.push_front({t, 0});
const TypeId& operator*();
seen.insert(t);
}
friend UnionTypeVarIterator end(const UnionTypeVar* utv);
TypeIterator<T>& operator++()
{
advance();
descend();
return *this;
}
TypeIterator<T> operator++(int)
{
TypeIterator<T> copy = *this;
++copy;
return copy;
}
bool operator==(const TypeIterator<T>& rhs) const
{
if (!stack.empty() && !rhs.stack.empty())
return stack.front() == rhs.stack.front();
return stack.empty() && rhs.stack.empty();
}
bool operator!=(const TypeIterator<T>& rhs) const
{
return !(*this == rhs);
}
const TypeId& operator*()
{
LUAU_ASSERT(!stack.empty());
descend();
auto [t, currentIndex] = stack.front();
LUAU_ASSERT(t);
const std::vector<TypeId>& types = getTypes(t);
LUAU_ASSERT(currentIndex < types.size());
const TypeId& ty = types[currentIndex];
LUAU_ASSERT(!get<T>(follow(ty)));
return ty;
}
// Normally, we'd have `begin` and `end` be a template but there's too much trouble
// with templates portability in this area, so not worth it. Thanks MSVC.
friend UnionTypeVarIterator end(const UnionTypeVar*);
friend IntersectionTypeVarIterator end(const IntersectionTypeVar*);
friend ConstrainedTypeVarIterator end(const ConstrainedTypeVar*);
private:
UnionTypeVarIterator() = default;
TypeIterator() = default;
// (UnionTypeVar* utv, size_t currentIndex)
using SavedIterInfo = std::pair<const UnionTypeVar*, size_t>;
// (T* t, size_t currentIndex)
using SavedIterInfo = std::pair<const T*, size_t>;
std::deque<SavedIterInfo> stack;
std::unordered_set<const UnionTypeVar*> seen; // Only needed to protect the iterator from hanging the thread.
std::unordered_set<const T*> seen; // Only needed to protect the iterator from hanging the thread.
void advance();
void descend();
void advance()
{
while (!stack.empty())
{
auto& [t, currentIndex] = stack.front();
++currentIndex;
const std::vector<TypeId>& types = getTypes(t);
if (currentIndex >= types.size())
stack.pop_front();
else
break;
}
}
void descend()
{
while (!stack.empty())
{
auto [current, currentIndex] = stack.front();
const std::vector<TypeId>& types = getTypes(current);
if (auto inner = get<T>(follow(types[currentIndex])))
{
// If we're about to descend into a cyclic type, we should skip over this.
// Ideally this should never happen, but alas it does from time to time. :(
if (seen.find(inner) != seen.end())
advance();
else
{
seen.insert(inner);
stack.push_front({inner, 0});
}
continue;
}
break;
}
}
};
UnionTypeVarIterator begin(const UnionTypeVar* utv);
UnionTypeVarIterator end(const UnionTypeVar* utv);
using TypeIdPredicate = std::function<std::optional<TypeId>(TypeId)>;
std::vector<TypeId> filterMap(TypeId type, TypeIdPredicate predicate);

View file

@ -129,6 +129,14 @@ struct GenericTypeVarVisitor
{
return visit(ty);
}
virtual bool visit(TypeId ty, const UnknownTypeVar& atv)
{
return visit(ty);
}
virtual bool visit(TypeId ty, const NeverTypeVar& atv)
{
return visit(ty);
}
virtual bool visit(TypeId ty, const UnionTypeVar& utv)
{
return visit(ty);

View file

@ -17,6 +17,104 @@ namespace Luau
namespace
{
struct AutocompleteNodeFinder : public AstVisitor
{
const Position pos;
std::vector<AstNode*> ancestry;
explicit AutocompleteNodeFinder(Position pos, AstNode* root)
: pos(pos)
{
}
bool visit(AstExpr* expr) override
{
if (expr->location.begin < pos && pos <= expr->location.end)
{
ancestry.push_back(expr);
return true;
}
return false;
}
bool visit(AstStat* stat) override
{
if (stat->location.begin < pos && pos <= stat->location.end)
{
ancestry.push_back(stat);
return true;
}
return false;
}
bool visit(AstType* type) override
{
if (type->location.begin < pos && pos <= type->location.end)
{
ancestry.push_back(type);
return true;
}
return false;
}
bool visit(AstTypeError* type) override
{
// For a missing type, match the whole range including the start position
if (type->isMissing && type->location.containsClosed(pos))
{
ancestry.push_back(type);
return true;
}
return false;
}
bool visit(class AstTypePack* typePack) override
{
return true;
}
bool visit(AstStatBlock* block) override
{
// If ancestry is empty, we are inspecting the root of the AST. Its extent is considered to be infinite.
if (ancestry.empty())
{
ancestry.push_back(block);
return true;
}
// AstExprIndexName nodes are nested outside-in, so we want the outermost node in the case of nested nodes.
// ex foo.bar.baz is represented in the AST as IndexName{ IndexName {foo, bar}, baz}
if (!ancestry.empty() && ancestry.back()->is<AstExprIndexName>())
return false;
// Type annotation error might intersect the block statement when the function header is being written,
// annotation takes priority
if (!ancestry.empty() && ancestry.back()->is<AstTypeError>())
return false;
// If the cursor is at the end of an expression or type and simultaneously at the beginning of a block,
// the expression or type wins out.
// The exception to this is if we are in a block under an AstExprFunction. In this case, we consider the position to
// be within the block.
if (block->location.begin == pos && !ancestry.empty())
{
if (ancestry.back()->asExpr() && !ancestry.back()->is<AstExprFunction>())
return false;
if (ancestry.back()->asType())
return false;
}
if (block->location.begin <= pos && pos <= block->location.end)
{
ancestry.push_back(block);
return true;
}
return false;
}
};
struct FindNode : public AstVisitor
{
const Position pos;
@ -102,6 +200,13 @@ struct FindFullAncestry final : public AstVisitor
} // namespace
std::vector<AstNode*> findAncestryAtPositionForAutocomplete(const SourceModule& source, Position pos)
{
AutocompleteNodeFinder finder{pos, source.root};
source.root->visit(&finder);
return finder.ancestry;
}
std::vector<AstNode*> findAstAncestryOfPosition(const SourceModule& source, Position pos)
{
const Position end = source.root->location.end;
@ -110,7 +215,7 @@ std::vector<AstNode*> findAstAncestryOfPosition(const SourceModule& source, Posi
FindFullAncestry finder(pos, end);
source.root->visit(&finder);
return std::move(finder.nodes);
return finder.nodes;
}
AstNode* findNodeAtPosition(const SourceModule& source, Position pos)

View file

@ -21,102 +21,6 @@ static const std::unordered_set<std::string> kStatementStartingKeywords = {
namespace Luau
{
struct NodeFinder : public AstVisitor
{
const Position pos;
std::vector<AstNode*> ancestry;
explicit NodeFinder(Position pos, AstNode* root)
: pos(pos)
{
}
bool visit(AstExpr* expr) override
{
if (expr->location.begin < pos && pos <= expr->location.end)
{
ancestry.push_back(expr);
return true;
}
return false;
}
bool visit(AstStat* stat) override
{
if (stat->location.begin < pos && pos <= stat->location.end)
{
ancestry.push_back(stat);
return true;
}
return false;
}
bool visit(AstType* type) override
{
if (type->location.begin < pos && pos <= type->location.end)
{
ancestry.push_back(type);
return true;
}
return false;
}
bool visit(AstTypeError* type) override
{
// For a missing type, match the whole range including the start position
if (type->isMissing && type->location.containsClosed(pos))
{
ancestry.push_back(type);
return true;
}
return false;
}
bool visit(class AstTypePack* typePack) override
{
return true;
}
bool visit(AstStatBlock* block) override
{
// If ancestry is empty, we are inspecting the root of the AST. Its extent is considered to be infinite.
if (ancestry.empty())
{
ancestry.push_back(block);
return true;
}
// AstExprIndexName nodes are nested outside-in, so we want the outermost node in the case of nested nodes.
// ex foo.bar.baz is represented in the AST as IndexName{ IndexName {foo, bar}, baz}
if (!ancestry.empty() && ancestry.back()->is<AstExprIndexName>())
return false;
// Type annotation error might intersect the block statement when the function header is being written,
// annotation takes priority
if (!ancestry.empty() && ancestry.back()->is<AstTypeError>())
return false;
// If the cursor is at the end of an expression or type and simultaneously at the beginning of a block,
// the expression or type wins out.
// The exception to this is if we are in a block under an AstExprFunction. In this case, we consider the position to
// be within the block.
if (block->location.begin == pos && !ancestry.empty())
{
if (ancestry.back()->asExpr() && !ancestry.back()->is<AstExprFunction>())
return false;
if (ancestry.back()->asType())
return false;
}
if (block->location.begin <= pos && pos <= block->location.end)
{
ancestry.push_back(block);
return true;
}
return false;
}
};
static bool alreadyHasParens(const std::vector<AstNode*>& nodes)
{
@ -905,7 +809,7 @@ AutocompleteEntryMap autocompleteTypeNames(const Module& module, Position positi
}
AstNode* parent = nullptr;
AstType* topType = nullptr;
AstType* topType = nullptr; // TODO: rename?
for (auto it = ancestry.rbegin(), e = ancestry.rend(); it != e; ++it)
{
@ -1477,21 +1381,20 @@ static AutocompleteResult autocomplete(const SourceModule& sourceModule, const M
if (isWithinComment(sourceModule, position))
return {};
NodeFinder finder{position, sourceModule.root};
sourceModule.root->visit(&finder);
LUAU_ASSERT(!finder.ancestry.empty());
AstNode* node = finder.ancestry.back();
std::vector<AstNode*> ancestry = findAncestryAtPositionForAutocomplete(sourceModule, position);
LUAU_ASSERT(!ancestry.empty());
AstNode* node = ancestry.back();
AstExprConstantNil dummy{Location{}};
AstNode* parent = finder.ancestry.size() >= 2 ? finder.ancestry.rbegin()[1] : &dummy;
AstNode* parent = ancestry.size() >= 2 ? ancestry.rbegin()[1] : &dummy;
// If we are inside a body of a function that doesn't have a completed argument list, ignore the body node
if (auto exprFunction = parent->as<AstExprFunction>(); exprFunction && !exprFunction->argLocation && node == exprFunction->body)
{
finder.ancestry.pop_back();
ancestry.pop_back();
node = finder.ancestry.back();
parent = finder.ancestry.size() >= 2 ? finder.ancestry.rbegin()[1] : &dummy;
node = ancestry.back();
parent = ancestry.size() >= 2 ? ancestry.rbegin()[1] : &dummy;
}
if (auto indexName = node->as<AstExprIndexName>())
@ -1504,47 +1407,47 @@ static AutocompleteResult autocomplete(const SourceModule& sourceModule, const M
PropIndexType indexType = indexName->op == ':' ? PropIndexType::Colon : PropIndexType::Point;
if (!FFlag::LuauSelfCallAutocompleteFix2 && isString(ty))
return {autocompleteProps(*module, typeArena, typeChecker.globalScope->bindings[AstName{"string"}].typeId, indexType, finder.ancestry),
finder.ancestry};
return {autocompleteProps(*module, typeArena, typeChecker.globalScope->bindings[AstName{"string"}].typeId, indexType, ancestry),
ancestry};
else
return {autocompleteProps(*module, typeArena, ty, indexType, finder.ancestry), finder.ancestry};
return {autocompleteProps(*module, typeArena, ty, indexType, ancestry), ancestry};
}
else if (auto typeReference = node->as<AstTypeReference>())
{
if (typeReference->prefix)
return {autocompleteModuleTypes(*module, position, typeReference->prefix->value), finder.ancestry};
return {autocompleteModuleTypes(*module, position, typeReference->prefix->value), ancestry};
else
return {autocompleteTypeNames(*module, position, finder.ancestry), finder.ancestry};
return {autocompleteTypeNames(*module, position, ancestry), ancestry};
}
else if (node->is<AstTypeError>())
{
return {autocompleteTypeNames(*module, position, finder.ancestry), finder.ancestry};
return {autocompleteTypeNames(*module, position, ancestry), ancestry};
}
else if (AstStatLocal* statLocal = node->as<AstStatLocal>())
{
if (statLocal->vars.size == 1 && (!statLocal->equalsSignLocation || position < statLocal->equalsSignLocation->begin))
return {{{"function", AutocompleteEntry{AutocompleteEntryKind::Keyword}}}, finder.ancestry};
return {{{"function", AutocompleteEntry{AutocompleteEntryKind::Keyword}}}, ancestry};
else if (statLocal->equalsSignLocation && position >= statLocal->equalsSignLocation->end)
return {autocompleteExpression(sourceModule, *module, typeChecker, typeArena, finder.ancestry, position), finder.ancestry};
return {autocompleteExpression(sourceModule, *module, typeChecker, typeArena, ancestry, position), ancestry};
else
return {};
}
else if (AstStatFor* statFor = extractStat<AstStatFor>(finder.ancestry))
else if (AstStatFor* statFor = extractStat<AstStatFor>(ancestry))
{
if (!statFor->hasDo || position < statFor->doLocation.begin)
{
if (!statFor->from->is<AstExprError>() && !statFor->to->is<AstExprError>() && (!statFor->step || !statFor->step->is<AstExprError>()))
return {{{"do", AutocompleteEntry{AutocompleteEntryKind::Keyword}}}, finder.ancestry};
return {{{"do", AutocompleteEntry{AutocompleteEntryKind::Keyword}}}, ancestry};
if (statFor->from->location.containsClosed(position) || statFor->to->location.containsClosed(position) ||
(statFor->step && statFor->step->location.containsClosed(position)))
return {autocompleteExpression(sourceModule, *module, typeChecker, typeArena, finder.ancestry, position), finder.ancestry};
return {autocompleteExpression(sourceModule, *module, typeChecker, typeArena, ancestry, position), ancestry};
return {};
}
return {autocompleteStatement(sourceModule, *module, finder.ancestry, position), finder.ancestry};
return {autocompleteStatement(sourceModule, *module, ancestry, position), ancestry};
}
else if (AstStatForIn* statForIn = parent->as<AstStatForIn>(); statForIn && (node->is<AstStatBlock>() || isIdentifier(node)))
@ -1560,7 +1463,7 @@ static AutocompleteResult autocomplete(const SourceModule& sourceModule, const M
return {};
}
return {{{"in", AutocompleteEntry{AutocompleteEntryKind::Keyword}}}, finder.ancestry};
return {{{"in", AutocompleteEntry{AutocompleteEntryKind::Keyword}}}, ancestry};
}
if (!statForIn->hasDo || position <= statForIn->doLocation.begin)
@ -1569,58 +1472,58 @@ static AutocompleteResult autocomplete(const SourceModule& sourceModule, const M
AstExpr* lastExpr = statForIn->values.data[statForIn->values.size - 1];
if (lastExpr->location.containsClosed(position))
return {autocompleteExpression(sourceModule, *module, typeChecker, typeArena, finder.ancestry, position), finder.ancestry};
return {autocompleteExpression(sourceModule, *module, typeChecker, typeArena, ancestry, position), ancestry};
if (position > lastExpr->location.end)
return {{{"do", AutocompleteEntry{AutocompleteEntryKind::Keyword}}}, finder.ancestry};
return {{{"do", AutocompleteEntry{AutocompleteEntryKind::Keyword}}}, ancestry};
return {}; // Not sure what this means
}
}
else if (AstStatForIn* statForIn = extractStat<AstStatForIn>(finder.ancestry))
else if (AstStatForIn* statForIn = extractStat<AstStatForIn>(ancestry))
{
// The AST looks a bit differently if the cursor is at a position where only the "do" keyword is allowed.
// ex "for f in f do"
if (!statForIn->hasDo)
return {{{"do", AutocompleteEntry{AutocompleteEntryKind::Keyword}}}, finder.ancestry};
return {{{"do", AutocompleteEntry{AutocompleteEntryKind::Keyword}}}, ancestry};
return {autocompleteStatement(sourceModule, *module, finder.ancestry, position), finder.ancestry};
return {autocompleteStatement(sourceModule, *module, ancestry, position), ancestry};
}
else if (AstStatWhile* statWhile = parent->as<AstStatWhile>(); node->is<AstStatBlock>() && statWhile)
{
if (!statWhile->hasDo && !statWhile->condition->is<AstStatError>() && position > statWhile->condition->location.end)
return {{{"do", AutocompleteEntry{AutocompleteEntryKind::Keyword}}}, finder.ancestry};
return {{{"do", AutocompleteEntry{AutocompleteEntryKind::Keyword}}}, ancestry};
if (!statWhile->hasDo || position < statWhile->doLocation.begin)
return {autocompleteExpression(sourceModule, *module, typeChecker, typeArena, finder.ancestry, position), finder.ancestry};
return {autocompleteExpression(sourceModule, *module, typeChecker, typeArena, ancestry, position), ancestry};
if (statWhile->hasDo && position > statWhile->doLocation.end)
return {autocompleteStatement(sourceModule, *module, finder.ancestry, position), finder.ancestry};
return {autocompleteStatement(sourceModule, *module, ancestry, position), ancestry};
}
else if (AstStatWhile* statWhile = extractStat<AstStatWhile>(finder.ancestry); statWhile && !statWhile->hasDo)
return {{{"do", AutocompleteEntry{AutocompleteEntryKind::Keyword}}}, finder.ancestry};
else if (AstStatWhile* statWhile = extractStat<AstStatWhile>(ancestry); statWhile && !statWhile->hasDo)
return {{{"do", AutocompleteEntry{AutocompleteEntryKind::Keyword}}}, ancestry};
else if (AstStatIf* statIf = node->as<AstStatIf>(); statIf && !statIf->elseLocation.has_value())
{
return {{{"else", AutocompleteEntry{AutocompleteEntryKind::Keyword}}, {"elseif", AutocompleteEntry{AutocompleteEntryKind::Keyword}}},
finder.ancestry};
ancestry};
}
else if (AstStatIf* statIf = parent->as<AstStatIf>(); statIf && node->is<AstStatBlock>())
{
if (statIf->condition->is<AstExprError>())
return {autocompleteExpression(sourceModule, *module, typeChecker, typeArena, finder.ancestry, position), finder.ancestry};
return {autocompleteExpression(sourceModule, *module, typeChecker, typeArena, ancestry, position), ancestry};
else if (!statIf->thenLocation || statIf->thenLocation->containsClosed(position))
return {{{"then", AutocompleteEntry{AutocompleteEntryKind::Keyword}}}, finder.ancestry};
return {{{"then", AutocompleteEntry{AutocompleteEntryKind::Keyword}}}, ancestry};
}
else if (AstStatIf* statIf = extractStat<AstStatIf>(finder.ancestry);
else if (AstStatIf* statIf = extractStat<AstStatIf>(ancestry);
statIf && (!statIf->thenLocation || statIf->thenLocation->containsClosed(position)))
return {{{"then", AutocompleteEntry{AutocompleteEntryKind::Keyword}}}, finder.ancestry};
return {{{"then", AutocompleteEntry{AutocompleteEntryKind::Keyword}}}, ancestry};
else if (AstStatRepeat* statRepeat = node->as<AstStatRepeat>(); statRepeat && statRepeat->condition->is<AstExprError>())
return {autocompleteExpression(sourceModule, *module, typeChecker, typeArena, finder.ancestry, position), finder.ancestry};
else if (AstStatRepeat* statRepeat = extractStat<AstStatRepeat>(finder.ancestry); statRepeat)
return {autocompleteStatement(sourceModule, *module, finder.ancestry, position), finder.ancestry};
return {autocompleteExpression(sourceModule, *module, typeChecker, typeArena, ancestry, position), ancestry};
else if (AstStatRepeat* statRepeat = extractStat<AstStatRepeat>(ancestry); statRepeat)
return {autocompleteStatement(sourceModule, *module, ancestry, position), ancestry};
else if (AstExprTable* exprTable = parent->as<AstExprTable>(); exprTable && (node->is<AstExprGlobal>() || node->is<AstExprConstantString>()))
{
for (const auto& [kind, key, value] : exprTable->items)
@ -1630,7 +1533,7 @@ static AutocompleteResult autocomplete(const SourceModule& sourceModule, const M
{
if (auto it = module->astExpectedTypes.find(exprTable))
{
auto result = autocompleteProps(*module, typeArena, *it, PropIndexType::Key, finder.ancestry);
auto result = autocompleteProps(*module, typeArena, *it, PropIndexType::Key, ancestry);
// Remove keys that are already completed
for (const auto& item : exprTable->items)
@ -1644,9 +1547,9 @@ static AutocompleteResult autocomplete(const SourceModule& sourceModule, const M
// If we know for sure that a key is being written, do not offer general expression suggestions
if (!key)
autocompleteExpression(sourceModule, *module, typeChecker, typeArena, finder.ancestry, position, result);
autocompleteExpression(sourceModule, *module, typeChecker, typeArena, ancestry, position, result);
return {result, finder.ancestry};
return {result, ancestry};
}
break;
@ -1654,11 +1557,11 @@ static AutocompleteResult autocomplete(const SourceModule& sourceModule, const M
}
}
else if (isIdentifier(node) && (parent->is<AstStatExpr>() || parent->is<AstStatError>()))
return {autocompleteStatement(sourceModule, *module, finder.ancestry, position), finder.ancestry};
return {autocompleteStatement(sourceModule, *module, ancestry, position), ancestry};
if (std::optional<AutocompleteEntryMap> ret = autocompleteStringParams(sourceModule, module, finder.ancestry, position, callback))
if (std::optional<AutocompleteEntryMap> ret = autocompleteStringParams(sourceModule, module, ancestry, position, callback))
{
return {*ret, finder.ancestry};
return {*ret, ancestry};
}
else if (node->is<AstExprConstantString>())
{
@ -1667,14 +1570,14 @@ static AutocompleteResult autocomplete(const SourceModule& sourceModule, const M
if (auto it = module->astExpectedTypes.find(node->asExpr()))
autocompleteStringSingleton(*it, false, result);
if (finder.ancestry.size() >= 2)
if (ancestry.size() >= 2)
{
if (auto idxExpr = finder.ancestry.at(finder.ancestry.size() - 2)->as<AstExprIndexExpr>())
if (auto idxExpr = ancestry.at(ancestry.size() - 2)->as<AstExprIndexExpr>())
{
if (auto it = module->astTypes.find(idxExpr->expr))
autocompleteProps(*module, typeArena, follow(*it), PropIndexType::Point, finder.ancestry, result);
autocompleteProps(*module, typeArena, follow(*it), PropIndexType::Point, ancestry, result);
}
else if (auto binExpr = finder.ancestry.at(finder.ancestry.size() - 2)->as<AstExprBinary>())
else if (auto binExpr = ancestry.at(ancestry.size() - 2)->as<AstExprBinary>())
{
if (binExpr->op == AstExprBinary::CompareEq || binExpr->op == AstExprBinary::CompareNe)
{
@ -1684,7 +1587,7 @@ static AutocompleteResult autocomplete(const SourceModule& sourceModule, const M
}
}
return {result, finder.ancestry};
return {result, ancestry};
}
if (node->is<AstExprConstantNumber>())
@ -1693,9 +1596,9 @@ static AutocompleteResult autocomplete(const SourceModule& sourceModule, const M
}
if (node->asExpr())
return {autocompleteExpression(sourceModule, *module, typeChecker, typeArena, finder.ancestry, position), finder.ancestry};
return {autocompleteExpression(sourceModule, *module, typeChecker, typeArena, ancestry, position), ancestry};
else if (node->asStat())
return {autocompleteStatement(sourceModule, *module, finder.ancestry, position), finder.ancestry};
return {autocompleteStatement(sourceModule, *module, ancestry, position), ancestry};
return {};
}

View file

@ -9,6 +9,7 @@
#include <algorithm>
LUAU_FASTFLAGVARIABLE(LuauSetMetaTableArgsCheck, false)
LUAU_FASTFLAG(LuauUnknownAndNeverType)
/** FIXME: Many of these type definitions are not quite completely accurate.
*
@ -222,14 +223,14 @@ void registerBuiltinTypes(TypeChecker& typeChecker)
addGlobalBinding(typeChecker, "getmetatable", makeFunction(arena, std::nullopt, {genericMT}, {}, {tableMetaMT}, {genericMT}), "@luau");
// setmetatable<MT>({ @metatable MT }, MT) -> { @metatable MT }
// clang-format off
// setmetatable<T: {}, MT>(T, MT) -> { @metatable MT, T }
addGlobalBinding(typeChecker, "setmetatable",
arena.addType(
FunctionTypeVar{
{genericMT},
{},
arena.addTypePack(TypePack{{tableMetaMT, genericMT}}),
arena.addTypePack(TypePack{{FFlag::LuauUnknownAndNeverType ? tabTy : tableMetaMT, genericMT}}),
arena.addTypePack(TypePack{{tableMetaMT}})
}
), "@luau"
@ -309,6 +310,12 @@ static std::optional<WithPredicate<TypePackId>> magicFunctionSetMetaTable(
{
auto [paramPack, _predicates] = withPredicate;
if (FFlag::LuauUnknownAndNeverType)
{
if (size(paramPack) < 2 && finite(paramPack))
return std::nullopt;
}
TypeArena& arena = typechecker.currentModule->internalTypes;
std::vector<TypeId> expectedArgs = typechecker.unTypePack(scope, paramPack, 2, expr.location);
@ -316,6 +323,12 @@ static std::optional<WithPredicate<TypePackId>> magicFunctionSetMetaTable(
TypeId target = follow(expectedArgs[0]);
TypeId mt = follow(expectedArgs[1]);
if (FFlag::LuauUnknownAndNeverType)
{
typechecker.tablify(target);
typechecker.tablify(mt);
}
if (const auto& tab = get<TableTypeVar>(target))
{
if (target->persistent)
@ -324,7 +337,8 @@ static std::optional<WithPredicate<TypePackId>> magicFunctionSetMetaTable(
}
else
{
typechecker.tablify(mt);
if (!FFlag::LuauUnknownAndNeverType)
typechecker.tablify(mt);
const TableTypeVar* mtTtv = get<TableTypeVar>(mt);
MetatableTypeVar mtv{target, mt};
@ -343,7 +357,10 @@ static std::optional<WithPredicate<TypePackId>> magicFunctionSetMetaTable(
if (FFlag::LuauSetMetaTableArgsCheck && expr.args.size < 1)
{
return WithPredicate<TypePackId>{};
if (FFlag::LuauUnknownAndNeverType)
return std::nullopt;
else
return WithPredicate<TypePackId>{};
}
if (!FFlag::LuauSetMetaTableArgsCheck || !expr.self)
@ -390,11 +407,21 @@ static std::optional<WithPredicate<TypePackId>> magicFunctionAssert(
if (head.size() > 0)
{
std::optional<TypeId> newhead = typechecker.pickTypesFromSense(head[0], true);
if (!newhead)
head = {typechecker.nilType};
auto [ty, ok] = typechecker.pickTypesFromSense(head[0], true);
if (FFlag::LuauUnknownAndNeverType)
{
if (get<NeverTypeVar>(*ty))
head = {*ty};
else
head[0] = *ty;
}
else
head[0] = *newhead;
{
if (!ty)
head = {typechecker.nilType};
else
head[0] = *ty;
}
}
return WithPredicate<TypePackId>{arena.addTypePack(TypePack{std::move(head), tail})};

View file

@ -59,6 +59,8 @@ struct TypeCloner
void operator()(const UnionTypeVar& t);
void operator()(const IntersectionTypeVar& t);
void operator()(const LazyTypeVar& t);
void operator()(const UnknownTypeVar& t);
void operator()(const NeverTypeVar& t);
};
struct TypePackCloner
@ -310,6 +312,16 @@ void TypeCloner::operator()(const LazyTypeVar& t)
defaultClone(t);
}
void TypeCloner::operator()(const UnknownTypeVar& t)
{
defaultClone(t);
}
void TypeCloner::operator()(const NeverTypeVar& t)
{
defaultClone(t);
}
} // anonymous namespace
TypePackId clone(TypePackId tp, TypeArena& dest, CloneState& cloneState)

View file

@ -1,6 +1,7 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#include "Luau/BuiltinDefinitions.h"
LUAU_FASTFLAG(LuauUnknownAndNeverType)
LUAU_FASTFLAG(LuauCheckLenMT)
namespace Luau
@ -116,8 +117,6 @@ declare function typeof<T>(value: T): string
-- `assert` has a magic function attached that will give more detailed type information
declare function assert<T>(value: T, errorMessage: string?): T
declare function error<T>(message: T, level: number?)
declare function tostring<T>(value: T): string
declare function tonumber<T>(value: T, radix: number?): number?
@ -204,12 +203,18 @@ declare function unpack<V>(tab: {V}, i: number?, j: number?): ...V
std::string getBuiltinDefinitionSource()
{
std::string result = kBuiltinDefinitionLuaSrc;
// TODO: move this into kBuiltinDefinitionLuaSrc
if (FFlag::LuauCheckLenMT)
result += "declare function rawlen<K, V>(obj: {[K]: V} | string): number\n";
if (FFlag::LuauUnknownAndNeverType)
result += "declare function error<T>(message: T, level: number?): never\n";
else
result += "declare function error<T>(message: T, level: number?)\n";
return result;
}

View file

@ -496,6 +496,8 @@ CheckResult Frontend::check(const ModuleName& name, std::optional<FrontendOption
module->astTypes.clear();
module->astExpectedTypes.clear();
module->astOriginalCallTypes.clear();
module->astResolvedTypes.clear();
module->astResolvedTypePacks.clear();
module->scopes.resize(1);
}

View file

@ -14,7 +14,7 @@ LUAU_FASTFLAGVARIABLE(DebugLuauCopyBeforeNormalizing, false)
LUAU_FASTINTVARIABLE(LuauNormalizeIterationLimit, 1200);
LUAU_FASTFLAGVARIABLE(LuauNormalizeCombineTableFix, false);
LUAU_FASTFLAGVARIABLE(LuauNormalizeFlagIsConservative, false);
LUAU_FASTFLAGVARIABLE(LuauNormalizeCombineEqFix, false);
LUAU_FASTFLAG(LuauUnknownAndNeverType)
LUAU_FASTFLAG(LuauQuantifyConstrained)
namespace Luau
@ -182,7 +182,6 @@ struct Normalize final : TypeVarVisitor
{
if (!ty->normal)
asMutable(ty)->normal = true;
return false;
}
@ -193,6 +192,20 @@ struct Normalize final : TypeVarVisitor
return false;
}
bool visit(TypeId ty, const UnknownTypeVar&) override
{
if (!ty->normal)
asMutable(ty)->normal = true;
return false;
}
bool visit(TypeId ty, const NeverTypeVar&) override
{
if (!ty->normal)
asMutable(ty)->normal = true;
return false;
}
bool visit(TypeId ty, const ConstrainedTypeVar& ctvRef) override
{
CHECK_ITERATION_LIMIT(false);
@ -416,7 +429,13 @@ struct Normalize final : TypeVarVisitor
std::vector<TypeId> result;
for (TypeId part : options)
{
// AnyTypeVar always win the battle no matter what we do, so we're done.
if (FFlag::LuauUnknownAndNeverType && get<AnyTypeVar>(follow(part)))
return {part};
combineIntoUnion(result, part);
}
return result;
}
@ -427,7 +446,17 @@ struct Normalize final : TypeVarVisitor
if (auto utv = get<UnionTypeVar>(ty))
{
for (TypeId t : utv)
{
// AnyTypeVar always win the battle no matter what we do, so we're done.
if (FFlag::LuauUnknownAndNeverType && get<AnyTypeVar>(t))
{
result = {t};
return;
}
combineIntoUnion(result, t);
}
return;
}
@ -571,8 +600,7 @@ struct Normalize final : TypeVarVisitor
*/
TypeId combine(Replacer& replacer, TypeId a, TypeId b)
{
if (FFlag::LuauNormalizeCombineEqFix)
b = follow(b);
b = follow(b);
if (FFlag::LuauNormalizeCombineTableFix && a == b)
return a;
@ -592,7 +620,7 @@ struct Normalize final : TypeVarVisitor
}
else if (auto ttv = getMutable<TableTypeVar>(a))
{
if (FFlag::LuauNormalizeCombineTableFix && !get<TableTypeVar>(FFlag::LuauNormalizeCombineEqFix ? b : follow(b)))
if (FFlag::LuauNormalizeCombineTableFix && !get<TableTypeVar>(b))
return arena.addType(IntersectionTypeVar{{a, b}});
combineIntoTable(replacer, ttv, b);
return a;

View file

@ -8,8 +8,10 @@
#include <algorithm>
#include <stdexcept>
LUAU_FASTFLAGVARIABLE(LuauAnyificationMustClone, false)
LUAU_FASTFLAG(LuauLowerBoundsCalculation)
LUAU_FASTINTVARIABLE(LuauTarjanChildLimit, 10000)
LUAU_FASTFLAG(LuauUnknownAndNeverType)
namespace Luau
{
@ -154,7 +156,7 @@ TarjanResult Tarjan::loop()
if (currEdge == -1)
{
++childCount;
if (childLimit > 0 && childLimit < childCount)
if (childLimit > 0 && (FFlag::LuauUnknownAndNeverType ? childLimit <= childCount : childLimit < childCount))
return TarjanResult::TooManyChildren;
stack.push_back(index);
@ -439,6 +441,9 @@ void Substitution::replaceChildren(TypeId ty)
if (ignoreChildren(ty))
return;
if (FFlag::LuauAnyificationMustClone && ty->owningArena != arena)
return;
if (FunctionTypeVar* ftv = getMutable<FunctionTypeVar>(ty))
{
ftv->argTypes = replace(ftv->argTypes);
@ -490,6 +495,9 @@ void Substitution::replaceChildren(TypePackId tp)
if (ignoreChildren(tp))
return;
if (FFlag::LuauAnyificationMustClone && tp->owningArena != arena)
return;
if (TypePack* tpp = getMutable<TypePack>(tp))
{
for (TypeId& tv : tpp->head)

View file

@ -11,6 +11,7 @@
#include <stdexcept>
LUAU_FASTFLAG(LuauLowerBoundsCalculation)
LUAU_FASTFLAG(LuauUnknownAndNeverType)
/*
* Prefix generic typenames with gen-
@ -699,6 +700,12 @@ struct TypeVarStringifier
void operator()(TypeId, const MetatableTypeVar& mtv)
{
state.result.invalid = true;
if (!state.exhaustive && mtv.syntheticName)
{
state.emit(*mtv.syntheticName);
return;
}
state.emit("{ @metatable ");
stringify(mtv.metatable);
state.emit(",");
@ -834,7 +841,7 @@ struct TypeVarStringifier
void operator()(TypeId, const ErrorTypeVar& tv)
{
state.result.error = true;
state.emit("*unknown*");
state.emit(FFlag::LuauUnknownAndNeverType ? "<error-type>" : "*unknown*");
}
void operator()(TypeId, const LazyTypeVar& ltv)
@ -843,7 +850,17 @@ struct TypeVarStringifier
state.emit("lazy?");
}
}; // namespace
void operator()(TypeId, const UnknownTypeVar& ttv)
{
state.emit("unknown");
}
void operator()(TypeId, const NeverTypeVar& ttv)
{
state.emit("never");
}
};
struct TypePackStringifier
{
@ -947,7 +964,7 @@ struct TypePackStringifier
void operator()(TypePackId, const Unifiable::Error& error)
{
state.result.error = true;
state.emit("*unknown*");
state.emit(FFlag::LuauUnknownAndNeverType ? "<error-type>" : "*unknown*");
}
void operator()(TypePackId, const VariadicTypePack& pack)

View file

@ -205,20 +205,6 @@ struct Printer
}
}
void visualizeWithSelf(AstExpr& expr, bool self)
{
if (!self)
return visualize(expr);
AstExprIndexName* func = expr.as<AstExprIndexName>();
LUAU_ASSERT(func);
visualize(*func->expr);
writer.symbol(":");
advance(func->indexLocation.begin);
writer.identifier(func->index.value);
}
void visualizeTypePackAnnotation(const AstTypePack& annotation, bool forVarArg)
{
advance(annotation.location.begin);
@ -366,7 +352,7 @@ struct Printer
}
else if (const auto& a = expr.as<AstExprCall>())
{
visualizeWithSelf(*a->func, a->self);
visualize(*a->func);
writer.symbol("(");
bool first = true;
@ -385,7 +371,7 @@ struct Printer
else if (const auto& a = expr.as<AstExprIndexName>())
{
visualize(*a->expr);
writer.symbol(".");
writer.symbol(std::string(1, a->op));
writer.write(a->index.value);
}
else if (const auto& a = expr.as<AstExprIndexExpr>())
@ -766,7 +752,7 @@ struct Printer
else if (const auto& a = program.as<AstStatFunction>())
{
writer.keyword("function");
visualizeWithSelf(*a->name, a->func->self != nullptr);
visualize(*a->name);
visualizeFunctionBody(*a->func);
}
else if (const auto& a = program.as<AstStatLocalFunction>())

View file

@ -7,7 +7,7 @@
#include <algorithm>
#include <stdexcept>
LUAU_FASTFLAG(LuauNonCopyableTypeVarFields)
LUAU_FASTFLAG(LuauUnknownAndNeverType)
namespace Luau
{
@ -81,34 +81,10 @@ void TxnLog::concat(TxnLog rhs)
void TxnLog::commit()
{
for (auto& [ty, rep] : typeVarChanges)
{
if (FFlag::LuauNonCopyableTypeVarFields)
{
asMutable(ty)->reassign(rep.get()->pending);
}
else
{
TypeArena* owningArena = ty->owningArena;
TypeVar* mtv = asMutable(ty);
*mtv = rep.get()->pending;
mtv->owningArena = owningArena;
}
}
asMutable(ty)->reassign(rep.get()->pending);
for (auto& [tp, rep] : typePackChanges)
{
if (FFlag::LuauNonCopyableTypeVarFields)
{
asMutable(tp)->reassign(rep.get()->pending);
}
else
{
TypeArena* owningArena = tp->owningArena;
TypePackVar* mpv = asMutable(tp);
*mpv = rep.get()->pending;
mpv->owningArena = owningArena;
}
}
asMutable(tp)->reassign(rep.get()->pending);
clear();
}
@ -196,9 +172,7 @@ PendingType* TxnLog::queue(TypeId ty)
if (!pending)
{
pending = std::make_unique<PendingType>(*ty);
if (FFlag::LuauNonCopyableTypeVarFields)
pending->pending.owningArena = nullptr;
pending->pending.owningArena = nullptr;
}
return pending.get();
@ -214,9 +188,7 @@ PendingTypePack* TxnLog::queue(TypePackId tp)
if (!pending)
{
pending = std::make_unique<PendingTypePack>(*tp);
if (FFlag::LuauNonCopyableTypeVarFields)
pending->pending.owningArena = nullptr;
pending->pending.owningArena = nullptr;
}
return pending.get();
@ -255,24 +227,14 @@ PendingTypePack* TxnLog::pending(TypePackId tp) const
PendingType* TxnLog::replace(TypeId ty, TypeVar replacement)
{
PendingType* newTy = queue(ty);
if (FFlag::LuauNonCopyableTypeVarFields)
newTy->pending.reassign(replacement);
else
newTy->pending = replacement;
newTy->pending.reassign(replacement);
return newTy;
}
PendingTypePack* TxnLog::replace(TypePackId tp, TypePackVar replacement)
{
PendingTypePack* newTp = queue(tp);
if (FFlag::LuauNonCopyableTypeVarFields)
newTp->pending.reassign(replacement);
else
newTp->pending = replacement;
newTp->pending.reassign(replacement);
return newTp;
}
@ -289,7 +251,7 @@ PendingType* TxnLog::bindTable(TypeId ty, std::optional<TypeId> newBoundTo)
PendingType* TxnLog::changeLevel(TypeId ty, TypeLevel newLevel)
{
LUAU_ASSERT(get<FreeTypeVar>(ty) || get<TableTypeVar>(ty) || get<FunctionTypeVar>(ty));
LUAU_ASSERT(get<FreeTypeVar>(ty) || get<TableTypeVar>(ty) || get<FunctionTypeVar>(ty) || get<ConstrainedTypeVar>(ty));
PendingType* newTy = queue(ty);
if (FreeTypeVar* ftv = Luau::getMutable<FreeTypeVar>(newTy))
@ -305,6 +267,11 @@ PendingType* TxnLog::changeLevel(TypeId ty, TypeLevel newLevel)
{
ftv->level = newLevel;
}
else if (ConstrainedTypeVar* ctv = Luau::getMutable<ConstrainedTypeVar>(newTy))
{
if (FFlag::LuauUnknownAndNeverType)
ctv->level = newLevel;
}
return newTy;
}

View file

@ -335,6 +335,14 @@ public:
{
return allocator->alloc<AstTypeReference>(Location(), std::nullopt, AstName("<Lazy?>"));
}
AstType* operator()(const UnknownTypeVar& ttv)
{
return allocator->alloc<AstTypeReference>(Location(), std::nullopt, AstName{"unknown"});
}
AstType* operator()(const NeverTypeVar& ttv)
{
return allocator->alloc<AstTypeReference>(Location(), std::nullopt, AstName{"never"});
}
private:
Allocator* allocator;

View file

@ -31,6 +31,7 @@ LUAU_FASTINTVARIABLE(LuauCheckRecursionLimit, 300)
LUAU_FASTINTVARIABLE(LuauVisitRecursionLimit, 500)
LUAU_FASTFLAG(LuauKnowsTheDataModel3)
LUAU_FASTFLAG(LuauAutocompleteDynamicLimits)
LUAU_FASTFLAGVARIABLE(LuauIndexSilenceErrors, false)
LUAU_FASTFLAGVARIABLE(LuauLowerBoundsCalculation, false)
LUAU_FASTFLAGVARIABLE(DebugLuauFreezeDuringUnification, false)
LUAU_FASTFLAGVARIABLE(LuauSelfCallAutocompleteFix2, false)
@ -41,10 +42,12 @@ LUAU_FASTFLAGVARIABLE(LuauReturnTypeInferenceInNonstrict, false)
LUAU_FASTFLAGVARIABLE(DebugLuauSharedSelf, false);
LUAU_FASTFLAGVARIABLE(LuauAlwaysQuantify, false);
LUAU_FASTFLAGVARIABLE(LuauReportErrorsOnIndexerKeyMismatch, false)
LUAU_FASTFLAGVARIABLE(LuauUnknownAndNeverType, false)
LUAU_FASTFLAG(LuauQuantifyConstrained)
LUAU_FASTFLAGVARIABLE(LuauFalsyPredicateReturnsNilInstead, false)
LUAU_FASTFLAGVARIABLE(LuauNonCopyableTypeVarFields, false)
LUAU_FASTFLAGVARIABLE(LuauCheckLenMT, false)
LUAU_FASTFLAGVARIABLE(LuauCheckGenericHOFTypes, false)
LUAU_FASTFLAGVARIABLE(LuauBinaryNeedsExpectedTypesToo, false)
namespace Luau
{
@ -258,7 +261,11 @@ TypeChecker::TypeChecker(ModuleResolver* resolver, InternalErrorReporter* iceHan
, booleanType(getSingletonTypes().booleanType)
, threadType(getSingletonTypes().threadType)
, anyType(getSingletonTypes().anyType)
, unknownType(getSingletonTypes().unknownType)
, neverType(getSingletonTypes().neverType)
, anyTypePack(getSingletonTypes().anyTypePack)
, neverTypePack(getSingletonTypes().neverTypePack)
, uninhabitableTypePack(getSingletonTypes().uninhabitableTypePack)
, duplicateTypeAliases{{false, {}}}
{
globalScope = std::make_shared<Scope>(globalTypes.addTypePack(TypePackVar{FreeTypePack{TypeLevel{}}}));
@ -269,6 +276,11 @@ TypeChecker::TypeChecker(ModuleResolver* resolver, InternalErrorReporter* iceHan
globalScope->exportedTypeBindings["string"] = TypeFun{{}, stringType};
globalScope->exportedTypeBindings["boolean"] = TypeFun{{}, booleanType};
globalScope->exportedTypeBindings["thread"] = TypeFun{{}, threadType};
if (FFlag::LuauUnknownAndNeverType)
{
globalScope->exportedTypeBindings["unknown"] = TypeFun{{}, unknownType};
globalScope->exportedTypeBindings["never"] = TypeFun{{}, neverType};
}
}
ModulePtr TypeChecker::check(const SourceModule& module, Mode mode, std::optional<ScopePtr> environmentScope)
@ -456,6 +468,59 @@ void TypeChecker::checkBlock(const ScopePtr& scope, const AstStatBlock& block)
}
}
struct InplaceDemoter : TypeVarOnceVisitor
{
TypeLevel newLevel;
TypeArena* arena;
InplaceDemoter(TypeLevel level, TypeArena* arena)
: newLevel(level)
, arena(arena)
{
}
bool demote(TypeId ty)
{
if (auto level = getMutableLevel(ty))
{
if (level->subsumesStrict(newLevel))
{
*level = newLevel;
return true;
}
}
return false;
}
bool visit(TypeId ty, const BoundTypeVar& btyRef) override
{
return true;
}
bool visit(TypeId ty) override
{
if (ty->owningArena != arena)
return false;
return demote(ty);
}
bool visit(TypePackId tp, const FreeTypePack& ftpRef) override
{
if (tp->owningArena != arena)
return false;
FreeTypePack* ftp = &const_cast<FreeTypePack&>(ftpRef);
if (ftp->level.subsumesStrict(newLevel))
{
ftp->level = newLevel;
return true;
}
return false;
}
};
void TypeChecker::checkBlockWithoutRecursionCheck(const ScopePtr& scope, const AstStatBlock& block)
{
int subLevel = 0;
@ -559,7 +624,7 @@ void TypeChecker::checkBlockWithoutRecursionCheck(const ScopePtr& scope, const A
tablify(baseTy);
if (!fun->func->self)
expectedType = getIndexTypeFromType(scope, baseTy, name->index.value, name->indexLocation, false);
expectedType = getIndexTypeFromType(scope, baseTy, name->index.value, name->indexLocation, /* addErrors= */ false);
else if (auto ttv = getMutableTableType(baseTy))
{
if (!baseTy->persistent && ttv->state != TableState::Sealed && !ttv->selfTy)
@ -579,7 +644,7 @@ void TypeChecker::checkBlockWithoutRecursionCheck(const ScopePtr& scope, const A
if (auto name = fun->name->as<AstExprIndexName>())
{
TypeId exprTy = checkExpr(scope, *name->expr).type;
expectedType = getIndexTypeFromType(scope, exprTy, name->index.value, name->indexLocation, false);
expectedType = getIndexTypeFromType(scope, exprTy, name->index.value, name->indexLocation, /* addErrors= */ false);
}
}
}
@ -634,15 +699,8 @@ LUAU_NOINLINE void TypeChecker::checkBlockTypeAliases(const ScopePtr& scope, std
TypeId type = bindings[name].type;
if (get<FreeTypeVar>(follow(type)))
{
if (FFlag::LuauNonCopyableTypeVarFields)
{
TypeVar* mty = asMutable(follow(type));
mty->reassign(*errorRecoveryType(anyType));
}
else
{
*asMutable(type) = *errorRecoveryType(anyType);
}
TypeVar* mty = asMutable(follow(type));
mty->reassign(*errorRecoveryType(anyType));
reportError(TypeError{typealias->location, OccursCheckFailed{}});
}
@ -1206,7 +1264,7 @@ void TypeChecker::check(const ScopePtr& scope, const AstStatForIn& forin)
iterTy = instantiate(scope, checkExpr(scope, *firstValue).type, firstValue->location);
}
if (std::optional<TypeId> iterMM = findMetatableEntry(iterTy, "__iter", firstValue->location))
if (std::optional<TypeId> iterMM = findMetatableEntry(iterTy, "__iter", firstValue->location, /* addErrors= */ true))
{
// if __iter metamethod is present, it will be called and the results are going to be called as if they are functions
// TODO: this needs to typecheck all returned values by __iter as if they were for loop arguments
@ -1253,7 +1311,7 @@ void TypeChecker::check(const ScopePtr& scope, const AstStatForIn& forin)
for (TypeId var : varTypes)
unify(varTy, var, forin.location);
if (!get<ErrorTypeVar>(iterTy) && !get<AnyTypeVar>(iterTy) && !get<FreeTypeVar>(iterTy))
if (!get<ErrorTypeVar>(iterTy) && !get<AnyTypeVar>(iterTy) && !get<FreeTypeVar>(iterTy) && !get<NeverTypeVar>(iterTy))
reportError(firstValue->location, CannotCallNonFunction{iterTy});
return check(loopScope, *forin.body);
@ -1350,7 +1408,7 @@ void TypeChecker::check(const ScopePtr& scope, TypeId ty, const ScopePtr& funSco
TypeId exprTy = checkExpr(scope, *name->expr).type;
TableTypeVar* ttv = getMutableTableType(exprTy);
if (!getIndexTypeFromType(scope, exprTy, name->index.value, name->indexLocation, false))
if (!getIndexTypeFromType(scope, exprTy, name->index.value, name->indexLocation, /* addErrors= */ false))
{
if (ttv || isTableIntersection(exprTy))
reportError(TypeError{function.location, CannotExtendTable{exprTy, CannotExtendTable::Property, name->index.value}});
@ -1376,6 +1434,12 @@ void TypeChecker::check(const ScopePtr& scope, TypeId ty, const ScopePtr& funSco
checkFunctionBody(funScope, ty, *function.func);
if (FFlag::LuauUnknownAndNeverType)
{
InplaceDemoter demoter{funScope->level, &currentModule->internalTypes};
demoter.traverse(ty);
}
if (ttv && ttv->state != TableState::Sealed)
ttv->props[name->index.value] = {follow(quantify(funScope, ty, name->indexLocation)), /* deprecated */ false, {}, name->indexLocation};
}
@ -1729,7 +1793,7 @@ WithPredicate<TypeId> TypeChecker::checkExpr(const ScopePtr& scope, const AstExp
else if (auto a = expr.as<AstExprUnary>())
result = checkExpr(scope, *a);
else if (auto a = expr.as<AstExprBinary>())
result = checkExpr(scope, *a);
result = checkExpr(scope, *a, FFlag::LuauBinaryNeedsExpectedTypesToo ? expectedType : std::nullopt);
else if (auto a = expr.as<AstExprTypeAssertion>())
result = checkExpr(scope, *a);
else if (auto a = expr.as<AstExprError>())
@ -1851,41 +1915,56 @@ WithPredicate<TypeId> TypeChecker::checkExpr(const ScopePtr& scope, const AstExp
lhsType = stripFromNilAndReport(lhsType, expr.expr->location);
if (std::optional<TypeId> ty = getIndexTypeFromType(scope, lhsType, name, expr.location, true))
if (std::optional<TypeId> ty = getIndexTypeFromType(scope, lhsType, name, expr.location, /* addErrors= */ true))
return {*ty};
return {errorRecoveryType(scope)};
}
std::optional<TypeId> TypeChecker::findTablePropertyRespectingMeta(TypeId lhsType, Name name, const Location& location)
std::optional<TypeId> TypeChecker::findTablePropertyRespectingMeta(TypeId lhsType, Name name, const Location& location, bool addErrors)
{
ErrorVec errors;
auto result = Luau::findTablePropertyRespectingMeta(errors, lhsType, name, location);
reportErrors(errors);
if (!FFlag::LuauIndexSilenceErrors || addErrors)
reportErrors(errors);
return result;
}
std::optional<TypeId> TypeChecker::findMetatableEntry(TypeId type, std::string entry, const Location& location)
std::optional<TypeId> TypeChecker::findMetatableEntry(TypeId type, std::string entry, const Location& location, bool addErrors)
{
ErrorVec errors;
auto result = Luau::findMetatableEntry(errors, type, entry, location);
reportErrors(errors);
if (!FFlag::LuauIndexSilenceErrors || addErrors)
reportErrors(errors);
return result;
}
std::optional<TypeId> TypeChecker::getIndexTypeFromType(
const ScopePtr& scope, TypeId type, const std::string& name, const Location& location, bool addErrors)
const ScopePtr& scope, TypeId type, const Name& name, const Location& location, bool addErrors)
{
size_t errorCount = currentModule->errors.size();
std::optional<TypeId> result = getIndexTypeFromTypeImpl(scope, type, name, location, addErrors);
if (FFlag::LuauIndexSilenceErrors && !addErrors)
LUAU_ASSERT(errorCount == currentModule->errors.size());
return result;
}
std::optional<TypeId> TypeChecker::getIndexTypeFromTypeImpl(
const ScopePtr& scope, TypeId type, const Name& name, const Location& location, bool addErrors)
{
type = follow(type);
if (get<ErrorTypeVar>(type) || get<AnyTypeVar>(type))
if (get<ErrorTypeVar>(type) || get<AnyTypeVar>(type) || get<NeverTypeVar>(type))
return type;
tablify(type);
if (isString(type))
{
std::optional<TypeId> mtIndex = findMetatableEntry(stringType, "__index", location);
std::optional<TypeId> mtIndex = findMetatableEntry(stringType, "__index", location, addErrors);
LUAU_ASSERT(mtIndex);
type = *mtIndex;
}
@ -1919,7 +1998,7 @@ std::optional<TypeId> TypeChecker::getIndexTypeFromType(
return result;
}
if (auto found = findTablePropertyRespectingMeta(type, name, location))
if (auto found = findTablePropertyRespectingMeta(type, name, location, addErrors))
return *found;
}
else if (const ClassTypeVar* cls = get<ClassTypeVar>(type))
@ -1941,7 +2020,7 @@ std::optional<TypeId> TypeChecker::getIndexTypeFromType(
if (get<AnyTypeVar>(follow(t)))
return t;
if (std::optional<TypeId> ty = getIndexTypeFromType(scope, t, name, location, false))
if (std::optional<TypeId> ty = getIndexTypeFromType(scope, t, name, location, /* addErrors= */ false))
goodOptions.push_back(*ty);
else
badOptions.push_back(t);
@ -1972,6 +2051,8 @@ std::optional<TypeId> TypeChecker::getIndexTypeFromType(
else
{
std::vector<TypeId> result = reduceUnion(goodOptions);
if (FFlag::LuauUnknownAndNeverType && result.empty())
return neverType;
if (result.size() == 1)
return result[0];
@ -1987,7 +2068,7 @@ std::optional<TypeId> TypeChecker::getIndexTypeFromType(
{
RecursionLimiter _rl(&recursionCount, FInt::LuauTypeInferRecursionLimit);
if (std::optional<TypeId> ty = getIndexTypeFromType(scope, t, name, location, false))
if (std::optional<TypeId> ty = getIndexTypeFromType(scope, t, name, location, /* addErrors= */ false))
parts.push_back(*ty);
}
@ -2017,6 +2098,9 @@ std::vector<TypeId> TypeChecker::reduceUnion(const std::vector<TypeId>& types)
for (TypeId t : types)
{
t = follow(t);
if (get<NeverTypeVar>(t))
continue;
if (get<ErrorTypeVar>(t) || get<AnyTypeVar>(t))
return {t};
@ -2028,6 +2112,8 @@ std::vector<TypeId> TypeChecker::reduceUnion(const std::vector<TypeId>& types)
{
if (FFlag::LuauNormalizeFlagIsConservative)
ty = follow(ty);
if (get<NeverTypeVar>(ty))
continue;
if (get<ErrorTypeVar>(ty) || get<AnyTypeVar>(ty))
return {ty};
@ -2041,6 +2127,8 @@ std::vector<TypeId> TypeChecker::reduceUnion(const std::vector<TypeId>& types)
for (TypeId ty : r)
{
ty = follow(ty);
if (get<NeverTypeVar>(ty))
continue;
if (get<ErrorTypeVar>(ty) || get<AnyTypeVar>(ty))
return {ty};
@ -2314,14 +2402,14 @@ WithPredicate<TypeId> TypeChecker::checkExpr(const ScopePtr& scope, const AstExp
return {booleanType, {NotPredicate{std::move(result.predicates)}}};
case AstExprUnary::Minus:
{
const bool operandIsAny = get<AnyTypeVar>(operandType) || get<ErrorTypeVar>(operandType);
const bool operandIsAny = get<AnyTypeVar>(operandType) || get<ErrorTypeVar>(operandType) || get<NeverTypeVar>(operandType);
if (operandIsAny)
return {operandType};
if (typeCouldHaveMetatable(operandType))
{
if (auto fnt = findMetatableEntry(operandType, "__unm", expr.location))
if (auto fnt = findMetatableEntry(operandType, "__unm", expr.location, /* addErrors= */ true))
{
TypeId actualFunctionType = instantiate(scope, *fnt, expr.location);
TypePackId arguments = addTypePack({operandType});
@ -2355,14 +2443,14 @@ WithPredicate<TypeId> TypeChecker::checkExpr(const ScopePtr& scope, const AstExp
operandType = stripFromNilAndReport(operandType, expr.location);
if (get<ErrorTypeVar>(operandType))
return {errorRecoveryType(scope)};
if (get<ErrorTypeVar>(operandType) || get<NeverTypeVar>(operandType))
return {!FFlag::LuauUnknownAndNeverType ? errorRecoveryType(scope) : operandType};
DenseHashSet<TypeId> seen{nullptr};
if (FFlag::LuauCheckLenMT && typeCouldHaveMetatable(operandType))
{
if (auto fnt = findMetatableEntry(operandType, "__len", expr.location))
if (auto fnt = findMetatableEntry(operandType, "__len", expr.location, /* addErrors= */ true))
{
TypeId actualFunctionType = instantiate(scope, *fnt, expr.location);
TypePackId arguments = addTypePack({operandType});
@ -2433,6 +2521,9 @@ TypeId TypeChecker::unionOfTypes(TypeId a, TypeId b, const Location& location, b
return a;
std::vector<TypeId> types = reduceUnion({a, b});
if (FFlag::LuauUnknownAndNeverType && types.empty())
return neverType;
if (types.size() == 1)
return types[0];
@ -2485,7 +2576,7 @@ TypeId TypeChecker::checkRelationalOperation(
// 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<AnyTypeVar>(lhsType) || get<ErrorTypeVar>(lhsType);
const bool lhsIsAny = get<AnyTypeVar>(lhsType) || get<ErrorTypeVar>(lhsType) || get<NeverTypeVar>(lhsType);
// Peephole check for `cond and a or b -> type(a)|type(b)`
// TODO: Kill this when singleton types arrive. :(
@ -2508,7 +2599,7 @@ TypeId TypeChecker::checkRelationalOperation(
if (isNonstrictMode() && (isNil(lhsType) || isNil(rhsType)))
return booleanType;
const bool rhsIsAny = get<AnyTypeVar>(rhsType) || get<ErrorTypeVar>(rhsType);
const bool rhsIsAny = get<AnyTypeVar>(rhsType) || get<ErrorTypeVar>(rhsType) || get<NeverTypeVar>(rhsType);
if (lhsIsAny || rhsIsAny)
return booleanType;
@ -2596,7 +2687,7 @@ TypeId TypeChecker::checkRelationalOperation(
if (leftMetatable)
{
std::optional<TypeId> metamethod = findMetatableEntry(lhsType, metamethodName, expr.location);
std::optional<TypeId> metamethod = findMetatableEntry(lhsType, metamethodName, expr.location, /* addErrors= */ true);
if (metamethod)
{
if (const FunctionTypeVar* ftv = get<FunctionTypeVar>(*metamethod))
@ -2757,9 +2848,9 @@ TypeId TypeChecker::checkBinaryOperation(
};
std::string op = opToMetaTableEntry(expr.op);
if (auto fnt = findMetatableEntry(lhsType, op, expr.location))
if (auto fnt = findMetatableEntry(lhsType, op, expr.location, /* addErrors= */ true))
return checkMetatableCall(*fnt, lhsType, rhsType);
if (auto fnt = findMetatableEntry(rhsType, op, expr.location))
if (auto fnt = findMetatableEntry(rhsType, op, expr.location, /* addErrors= */ true))
{
// Note the intentionally reversed arguments here.
return checkMetatableCall(*fnt, rhsType, lhsType);
@ -2793,27 +2884,27 @@ TypeId TypeChecker::checkBinaryOperation(
}
}
WithPredicate<TypeId> TypeChecker::checkExpr(const ScopePtr& scope, const AstExprBinary& expr)
WithPredicate<TypeId> TypeChecker::checkExpr(const ScopePtr& scope, const AstExprBinary& expr, std::optional<TypeId> expectedType)
{
if (expr.op == AstExprBinary::And)
{
auto [lhsTy, lhsPredicates] = checkExpr(scope, *expr.left);
auto [lhsTy, lhsPredicates] = checkExpr(scope, *expr.left, expectedType);
ScopePtr innerScope = childScope(scope, expr.location);
resolve(lhsPredicates, innerScope, true);
auto [rhsTy, rhsPredicates] = checkExpr(innerScope, *expr.right);
auto [rhsTy, rhsPredicates] = checkExpr(innerScope, *expr.right, expectedType);
return {checkBinaryOperation(scope, expr, lhsTy, rhsTy), {AndPredicate{std::move(lhsPredicates), std::move(rhsPredicates)}}};
}
else if (expr.op == AstExprBinary::Or)
{
auto [lhsTy, lhsPredicates] = checkExpr(scope, *expr.left);
auto [lhsTy, lhsPredicates] = checkExpr(scope, *expr.left, expectedType);
ScopePtr innerScope = childScope(scope, expr.location);
resolve(lhsPredicates, innerScope, false);
auto [rhsTy, rhsPredicates] = checkExpr(innerScope, *expr.right);
auto [rhsTy, rhsPredicates] = checkExpr(innerScope, *expr.right, expectedType);
// Because of C++, I'm not sure if lhsPredicates was not moved out by the time we call checkBinaryOperation.
TypeId result = checkBinaryOperation(scope, expr, lhsTy, rhsTy, lhsPredicates);
@ -2824,6 +2915,8 @@ WithPredicate<TypeId> TypeChecker::checkExpr(const ScopePtr& scope, const AstExp
if (auto predicate = tryGetTypeGuardPredicate(expr))
return {booleanType, {std::move(*predicate)}};
// For these, passing expectedType is worse than simply forcing them, because their implementation
// may inadvertently check if expectedTypes exist first and use it, instead of forceSingleton first.
WithPredicate<TypeId> lhs = checkExpr(scope, *expr.left, std::nullopt, /*forceSingleton=*/true);
WithPredicate<TypeId> rhs = checkExpr(scope, *expr.right, std::nullopt, /*forceSingleton=*/true);
@ -2842,6 +2935,7 @@ WithPredicate<TypeId> TypeChecker::checkExpr(const ScopePtr& scope, const AstExp
}
else
{
// Expected types are not useful for other binary operators.
WithPredicate<TypeId> lhs = checkExpr(scope, *expr.left);
WithPredicate<TypeId> rhs = checkExpr(scope, *expr.right);
@ -2896,6 +2990,8 @@ WithPredicate<TypeId> TypeChecker::checkExpr(const ScopePtr& scope, const AstExp
return {trueType.type};
std::vector<TypeId> types = reduceUnion({trueType.type, falseType.type});
if (FFlag::LuauUnknownAndNeverType && types.empty())
return {neverType};
return {types.size() == 1 ? types[0] : addType(UnionTypeVar{std::move(types)})};
}
@ -2927,7 +3023,10 @@ TypeId TypeChecker::checkLValueBinding(const ScopePtr& scope, const AstExpr& exp
TypeId TypeChecker::checkLValueBinding(const ScopePtr& scope, const AstExprLocal& expr)
{
if (std::optional<TypeId> ty = scope->lookup(expr.local))
return *ty;
{
ty = follow(*ty);
return get<NeverTypeVar>(*ty) ? unknownType : *ty;
}
reportError(expr.location, UnknownSymbol{expr.local->name.value, UnknownSymbol::Binding});
return errorRecoveryType(scope);
@ -2941,7 +3040,10 @@ TypeId TypeChecker::checkLValueBinding(const ScopePtr& scope, const AstExprGloba
const auto it = moduleScope->bindings.find(expr.name);
if (it != moduleScope->bindings.end())
return it->second.typeId;
{
TypeId ty = follow(it->second.typeId);
return get<NeverTypeVar>(ty) ? unknownType : ty;
}
TypeId result = freshType(scope);
Binding& binding = moduleScope->bindings[expr.name];
@ -2962,6 +3064,9 @@ TypeId TypeChecker::checkLValueBinding(const ScopePtr& scope, const AstExprIndex
if (get<ErrorTypeVar>(lhs) || get<AnyTypeVar>(lhs))
return lhs;
if (get<NeverTypeVar>(lhs))
return unknownType;
tablify(lhs);
Name name = expr.index.value;
@ -3023,7 +3128,7 @@ TypeId TypeChecker::checkLValueBinding(const ScopePtr& scope, const AstExprIndex
}
else if (get<IntersectionTypeVar>(lhs))
{
if (std::optional<TypeId> ty = getIndexTypeFromType(scope, lhs, name, expr.location, false))
if (std::optional<TypeId> ty = getIndexTypeFromType(scope, lhs, name, expr.location, /* addErrors= */ false))
return *ty;
// If intersection has a table part, report that it cannot be extended just as a sealed table
@ -3050,6 +3155,9 @@ TypeId TypeChecker::checkLValueBinding(const ScopePtr& scope, const AstExprIndex
if (get<AnyTypeVar>(exprType) || get<ErrorTypeVar>(exprType))
return exprType;
if (get<NeverTypeVar>(exprType))
return unknownType;
AstExprConstantString* value = expr.index->as<AstExprConstantString>();
if (value)
@ -3156,7 +3264,7 @@ TypeId TypeChecker::checkFunctionName(const ScopePtr& scope, AstExpr& funName, T
if (!ttv || ttv->state == TableState::Sealed)
{
if (auto ty = getIndexTypeFromType(scope, lhsType, indexName->index.value, indexName->indexLocation, false))
if (auto ty = getIndexTypeFromType(scope, lhsType, indexName->index.value, indexName->indexLocation, /* addErrors= */ false))
return *ty;
return errorRecoveryType(scope);
@ -3228,9 +3336,12 @@ std::pair<TypeId, ScopePtr> TypeChecker::checkFunctionSignature(const ScopePtr&
}
}
// 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;
if (!FFlag::LuauCheckGenericHOFTypes)
{
// 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);
@ -3240,7 +3351,8 @@ std::pair<TypeId, ScopePtr> TypeChecker::checkFunctionSignature(const ScopePtr&
retPack = resolveTypePack(funScope, *expr.returnAnnotation);
else if (FFlag::LuauReturnTypeInferenceInNonstrict ? (!FFlag::LuauLowerBoundsCalculation && isNonstrictMode()) : isNonstrictMode())
retPack = anyTypePack;
else if (expectedFunctionType)
else if (expectedFunctionType &&
(!FFlag::LuauCheckGenericHOFTypes || (expectedFunctionType->generics.empty() && expectedFunctionType->genericPacks.empty())))
{
auto [head, tail] = flatten(expectedFunctionType->retTypes);
@ -3371,16 +3483,50 @@ std::pair<TypeId, ScopePtr> TypeChecker::checkFunctionSignature(const ScopePtr&
defn.originalNameLocation = originalName.value_or(Location(expr.location.begin, 0));
std::vector<TypeId> genericTys;
genericTys.reserve(generics.size());
std::transform(generics.begin(), generics.end(), std::back_inserter(genericTys), [](auto&& el) {
return el.ty;
});
// if we have a generic expected function type and no generics, we should use the expected ones.
if (FFlag::LuauCheckGenericHOFTypes)
{
if (expectedFunctionType && generics.empty())
{
genericTys = expectedFunctionType->generics;
}
else
{
genericTys.reserve(generics.size());
for (const GenericTypeDefinition& generic : generics)
genericTys.push_back(generic.ty);
}
}
else
{
genericTys.reserve(generics.size());
std::transform(generics.begin(), generics.end(), std::back_inserter(genericTys), [](auto&& el) {
return el.ty;
});
}
std::vector<TypePackId> genericTps;
genericTps.reserve(genericPacks.size());
std::transform(genericPacks.begin(), genericPacks.end(), std::back_inserter(genericTps), [](auto&& el) {
return el.tp;
});
// if we have a generic expected function type and no generic typepacks, we should use the expected ones.
if (FFlag::LuauCheckGenericHOFTypes)
{
if (expectedFunctionType && genericPacks.empty())
{
genericTps = expectedFunctionType->genericPacks;
}
else
{
genericTps.reserve(genericPacks.size());
for (const GenericTypePackDefinition& generic : genericPacks)
genericTps.push_back(generic.tp);
}
}
else
{
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)));
@ -3474,9 +3620,22 @@ void TypeChecker::checkFunctionBody(const ScopePtr& scope, TypeId ty, const AstE
}
WithPredicate<TypePackId> TypeChecker::checkExprPack(const ScopePtr& scope, const AstExpr& expr)
{
if (FFlag::LuauUnknownAndNeverType)
{
WithPredicate<TypePackId> result = checkExprPackHelper(scope, expr);
if (containsNever(result.type))
return {uninhabitableTypePack};
return result;
}
else
return checkExprPackHelper(scope, expr);
}
WithPredicate<TypePackId> TypeChecker::checkExprPackHelper(const ScopePtr& scope, const AstExpr& expr)
{
if (auto a = expr.as<AstExprCall>())
return checkExprPack(scope, *a);
return checkExprPackHelper(scope, *a);
else if (expr.is<AstExprVarargs>())
{
if (!scope->varargPack)
@ -3739,7 +3898,7 @@ void TypeChecker::checkArgumentList(
}
}
WithPredicate<TypePackId> TypeChecker::checkExprPack(const ScopePtr& scope, const AstExprCall& expr)
WithPredicate<TypePackId> TypeChecker::checkExprPackHelper(const ScopePtr& scope, const AstExprCall& expr)
{
// evaluate type of function
// decompose an intersection into its component overloads
@ -3763,7 +3922,7 @@ WithPredicate<TypePackId> TypeChecker::checkExprPack(const ScopePtr& scope, cons
selfType = checkExpr(scope, *indexExpr->expr).type;
selfType = stripFromNilAndReport(selfType, expr.func->location);
if (std::optional<TypeId> propTy = getIndexTypeFromType(scope, selfType, indexExpr->index.value, expr.location, true))
if (std::optional<TypeId> propTy = getIndexTypeFromType(scope, selfType, indexExpr->index.value, expr.location, /* addErrors= */ true))
{
functionType = *propTy;
actualFunctionType = instantiate(scope, functionType, expr.func->location);
@ -3813,11 +3972,25 @@ WithPredicate<TypePackId> TypeChecker::checkExprPack(const ScopePtr& scope, cons
if (get<Unifiable::Error>(argPack))
return {errorRecoveryTypePack(scope)};
TypePack* args = getMutable<TypePack>(argPack);
LUAU_ASSERT(args != nullptr);
TypePack* args = nullptr;
if (FFlag::LuauUnknownAndNeverType)
{
if (expr.self)
{
argPack = addTypePack(TypePack{{selfType}, argPack});
argListResult.type = argPack;
}
args = getMutable<TypePack>(argPack);
LUAU_ASSERT(args);
}
else
{
args = getMutable<TypePack>(argPack);
LUAU_ASSERT(args != nullptr);
if (expr.self)
args->head.insert(args->head.begin(), selfType);
if (expr.self)
args->head.insert(args->head.begin(), selfType);
}
std::vector<Location> argLocations;
argLocations.reserve(expr.args.size + 1);
@ -3876,7 +4049,10 @@ std::vector<std::optional<TypeId>> TypeChecker::getExpectedTypesForCall(const st
else
{
std::vector<TypeId> result = reduceUnion({*el, ty});
el = result.size() == 1 ? result[0] : addType(UnionTypeVar{std::move(result)});
if (FFlag::LuauUnknownAndNeverType && result.empty())
el = neverType;
else
el = result.size() == 1 ? result[0] : addType(UnionTypeVar{std::move(result)});
}
}
};
@ -3930,6 +4106,9 @@ std::optional<WithPredicate<TypePackId>> TypeChecker::checkCallOverload(const Sc
return {{errorRecoveryTypePack(scope)}};
}
if (get<NeverTypeVar>(fn))
return {{uninhabitableTypePack}};
if (auto ftv = get<FreeTypeVar>(fn))
{
// fn is one of the overloads of actualFunctionType, which
@ -3975,7 +4154,7 @@ std::optional<WithPredicate<TypePackId>> TypeChecker::checkCallOverload(const Sc
// Might be a callable table
if (const MetatableTypeVar* mttv = get<MetatableTypeVar>(fn))
{
if (std::optional<TypeId> ty = getIndexTypeFromType(scope, mttv->metatable, "__call", expr.func->location, false))
if (std::optional<TypeId> ty = getIndexTypeFromType(scope, mttv->metatable, "__call", expr.func->location, /* addErrors= */ false))
{
// Construct arguments with 'self' added in front
TypePackId metaCallArgPack = addTypePack(TypePackVar(TypePack{args->head, args->tail}));
@ -4202,6 +4381,7 @@ void TypeChecker::reportOverloadResolutionError(const ScopePtr& scope, const Ast
WithPredicate<TypePackId> TypeChecker::checkExprList(const ScopePtr& scope, const Location& location, const AstArray<AstExpr*>& exprs,
bool substituteFreeForNil, const std::vector<bool>& instantiateGenerics, const std::vector<std::optional<TypeId>>& expectedTypes)
{
bool uninhabitable = false;
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?
@ -4232,7 +4412,13 @@ WithPredicate<TypePackId> TypeChecker::checkExprList(const ScopePtr& scope, cons
auto [typePack, exprPredicates] = checkExprPack(scope, *expr);
insert(exprPredicates);
if (std::optional<TypeId> firstTy = first(typePack))
if (FFlag::LuauUnknownAndNeverType && containsNever(typePack))
{
// f(), g() where f() returns (never, string) or (string, never) means this whole TypePackId is uninhabitable, so return (never, ...never)
uninhabitable = true;
continue;
}
else if (std::optional<TypeId> firstTy = first(typePack))
{
if (!currentModule->astTypes.find(expr))
currentModule->astTypes[expr] = follow(*firstTy);
@ -4248,6 +4434,13 @@ WithPredicate<TypePackId> TypeChecker::checkExprList(const ScopePtr& scope, cons
auto [type, exprPredicates] = checkExpr(scope, *expr, expectedType);
insert(exprPredicates);
if (FFlag::LuauUnknownAndNeverType && get<NeverTypeVar>(type))
{
// f(), g() where f() returns (never, string) or (string, never) means this whole TypePackId is uninhabitable, so return (never, ...never)
uninhabitable = true;
continue;
}
TypeId actualType = substituteFreeForNil && expr->is<AstExprConstantNil>() ? freshType(scope) : type;
if (instantiateGenerics.size() > i && instantiateGenerics[i])
@ -4272,6 +4465,8 @@ WithPredicate<TypePackId> TypeChecker::checkExprList(const ScopePtr& scope, cons
for (TxnLog& log : inverseLogs)
log.commit();
if (FFlag::LuauUnknownAndNeverType && uninhabitable)
return {uninhabitableTypePack};
return {pack, predicates};
}
@ -4830,7 +5025,7 @@ TypeIdPredicate TypeChecker::mkTruthyPredicate(bool sense)
};
}
std::optional<TypeId> TypeChecker::filterMap(TypeId type, TypeIdPredicate predicate)
std::optional<TypeId> TypeChecker::filterMapImpl(TypeId type, TypeIdPredicate predicate)
{
std::vector<TypeId> types = Luau::filterMap(type, predicate);
if (!types.empty())
@ -4838,7 +5033,21 @@ std::optional<TypeId> TypeChecker::filterMap(TypeId type, TypeIdPredicate predic
return std::nullopt;
}
std::optional<TypeId> TypeChecker::pickTypesFromSense(TypeId type, bool sense)
std::pair<std::optional<TypeId>, bool> TypeChecker::filterMap(TypeId type, TypeIdPredicate predicate)
{
if (FFlag::LuauUnknownAndNeverType)
{
TypeId ty = filterMapImpl(type, predicate).value_or(neverType);
return {ty, !bool(get<NeverTypeVar>(ty))};
}
else
{
std::optional<TypeId> ty = filterMapImpl(type, predicate);
return {ty, bool(ty)};
}
}
std::pair<std::optional<TypeId>, bool> TypeChecker::pickTypesFromSense(TypeId type, bool sense)
{
return filterMap(type, mkTruthyPredicate(sense));
}
@ -4884,6 +5093,13 @@ TypePackId TypeChecker::freshTypePack(TypeLevel level)
}
TypeId TypeChecker::resolveType(const ScopePtr& scope, const AstType& annotation)
{
TypeId ty = resolveTypeWorker(scope, annotation);
currentModule->astResolvedTypes[&annotation] = ty;
return ty;
}
TypeId TypeChecker::resolveTypeWorker(const ScopePtr& scope, const AstType& annotation)
{
if (const auto& lit = annotation.as<AstTypeReference>())
{
@ -5200,9 +5416,10 @@ TypePackId TypeChecker::resolveTypePack(const ScopePtr& scope, const AstTypeList
TypePackId TypeChecker::resolveTypePack(const ScopePtr& scope, const AstTypePack& annotation)
{
TypePackId result;
if (const AstTypePackVariadic* variadic = annotation.as<AstTypePackVariadic>())
{
return addTypePack(TypePackVar{VariadicTypePack{resolveType(scope, *variadic->variadicType)}});
result = addTypePack(TypePackVar{VariadicTypePack{resolveType(scope, *variadic->variadicType)}});
}
else if (const AstTypePackGeneric* generic = annotation.as<AstTypePackGeneric>())
{
@ -5216,10 +5433,12 @@ TypePackId TypeChecker::resolveTypePack(const ScopePtr& scope, const AstTypePack
else
reportError(TypeError{generic->location, UnknownSymbol{genericName, UnknownSymbol::Type}});
return errorRecoveryTypePack(scope);
result = errorRecoveryTypePack(scope);
}
else
{
result = *genericTy;
}
return *genericTy;
}
else if (const AstTypePackExplicit* explicitTp = annotation.as<AstTypePackExplicit>())
{
@ -5229,14 +5448,17 @@ TypePackId TypeChecker::resolveTypePack(const ScopePtr& scope, const AstTypePack
types.push_back(resolveType(scope, *type));
if (auto tailType = explicitTp->typeList.tailType)
return addTypePack(types, resolveTypePack(scope, *tailType));
return addTypePack(types);
result = addTypePack(types, resolveTypePack(scope, *tailType));
else
result = addTypePack(types);
}
else
{
ice("Unknown AstTypePack kind");
}
currentModule->astResolvedTypePacks[&annotation] = result;
return result;
}
bool ApplyTypeFunction::isDirty(TypeId ty)
@ -5452,10 +5674,18 @@ void TypeChecker::refineLValue(const LValue& lvalue, RefinementMap& refis, const
// 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<TypeId> result = filterMap(*ty, predicate))
auto [result, ok] = filterMap(*ty, predicate);
if (FFlag::LuauUnknownAndNeverType)
{
addRefinement(refis, *target, *result);
}
else
addRefinement(refis, *target, errorRecoveryType(scope));
{
if (ok)
addRefinement(refis, *target, *result);
else
addRefinement(refis, *target, errorRecoveryType(scope));
}
return;
}
@ -5471,17 +5701,29 @@ void TypeChecker::refineLValue(const LValue& lvalue, RefinementMap& refis, const
{
std::optional<TypeId> discriminantTy;
if (auto field = Luau::get<Field>(*key)) // need to fully qualify Luau::get because of ADL.
discriminantTy = getIndexTypeFromType(scope, option, field->key, Location(), false);
discriminantTy = getIndexTypeFromType(scope, option, field->key, Location(), /* addErrors= */ false);
else
LUAU_ASSERT(!"Unhandled LValue alternative?");
if (!discriminantTy)
return; // Do nothing. An error was already reported, as per usual.
if (std::optional<TypeId> result = filterMap(*discriminantTy, predicate))
auto [result, ok] = filterMap(*discriminantTy, predicate);
if (FFlag::LuauUnknownAndNeverType)
{
viableTargetOptions.insert(option);
viableChildOptions.insert(*result);
if (!get<NeverTypeVar>(*result))
{
viableTargetOptions.insert(option);
viableChildOptions.insert(*result);
}
}
else
{
if (ok)
{
viableTargetOptions.insert(option);
viableChildOptions.insert(*result);
}
}
}
@ -5560,7 +5802,7 @@ std::optional<TypeId> TypeChecker::resolveLValue(const ScopePtr& scope, const LV
continue;
else if (auto field = get<Field>(key))
{
found = getIndexTypeFromType(scope, *found, field->key, Location(), false);
found = getIndexTypeFromType(scope, *found, field->key, Location(), /* addErrors= */ false);
if (!found)
return std::nullopt; // Turns out this type doesn't have the property at all. We're done.
}
@ -5740,6 +5982,9 @@ void TypeChecker::resolve(const TypeGuardPredicate& typeguardP, RefinementMap& r
auto mkFilter = [](ConditionFunc f, std::optional<TypeId> other = std::nullopt) -> SenseToTypeIdPredicate {
return [f, other](bool sense) -> TypeIdPredicate {
return [f, other, sense](TypeId ty) -> std::optional<TypeId> {
if (FFlag::LuauUnknownAndNeverType && sense && get<UnknownTypeVar>(ty))
return other.value_or(ty);
if (f(ty) == sense)
return ty;
@ -5847,8 +6092,15 @@ std::vector<TypeId> TypeChecker::unTypePack(const ScopePtr& scope, TypePackId tp
for (size_t i = 0; i < expectedLength; ++i)
expectedPack->head.push_back(freshType(scope));
size_t oldErrorsSize = currentModule->errors.size();
unify(tp, expectedTypePack, location);
// HACK: tryUnify would undo the changes to the expectedTypePack if the length mismatches, but
// we want to tie up free types to be error types, so we do this instead.
if (FFlag::LuauUnknownAndNeverType)
currentModule->errors.resize(oldErrorsSize);
for (TypeId& tp : expectedPack->head)
tp = follow(tp);

View file

@ -5,8 +5,6 @@
#include <stdexcept>
LUAU_FASTFLAG(LuauNonCopyableTypeVarFields)
namespace Luau
{
@ -40,19 +38,10 @@ TypePackVar& TypePackVar::operator=(TypePackVariant&& tp)
TypePackVar& TypePackVar::operator=(const TypePackVar& rhs)
{
if (FFlag::LuauNonCopyableTypeVarFields)
{
LUAU_ASSERT(owningArena == rhs.owningArena);
LUAU_ASSERT(!rhs.persistent);
LUAU_ASSERT(owningArena == rhs.owningArena);
LUAU_ASSERT(!rhs.persistent);
reassign(rhs);
}
else
{
ty = rhs.ty;
persistent = rhs.persistent;
owningArena = rhs.owningArena;
}
reassign(rhs);
return *this;
}
@ -294,6 +283,16 @@ std::optional<TypeId> first(TypePackId tp, bool ignoreHiddenVariadics)
return std::nullopt;
}
TypePackVar* asMutable(TypePackId tp)
{
return const_cast<TypePackVar*>(tp);
}
TypePack* asMutable(const TypePack* tp)
{
return const_cast<TypePack*>(tp);
}
bool isEmpty(TypePackId tp)
{
tp = follow(tp);
@ -360,13 +359,25 @@ bool isVariadic(TypePackId tp, const TxnLog& log)
return false;
}
TypePackVar* asMutable(TypePackId tp)
bool containsNever(TypePackId tp)
{
return const_cast<TypePackVar*>(tp);
auto it = begin(tp);
auto endIt = end(tp);
while (it != endIt)
{
if (get<NeverTypeVar>(follow(*it)))
return true;
++it;
}
if (auto tail = it.tail())
{
if (auto vtp = get<VariadicTypePack>(*tail); vtp && get<NeverTypeVar>(follow(vtp->ty)))
return true;
}
return false;
}
TypePack* asMutable(const TypePack* tp)
{
return const_cast<TypePack*>(tp);
}
} // namespace Luau

View file

@ -24,7 +24,7 @@ std::optional<TypeId> findMetatableEntry(ErrorVec& errors, TypeId type, std::str
const TableTypeVar* mtt = getTableType(unwrapped);
if (!mtt)
{
errors.push_back(TypeError{location, GenericError{"Metatable was not a table."}});
errors.push_back(TypeError{location, GenericError{"Metatable was not a table"}});
return std::nullopt;
}

View file

@ -23,7 +23,9 @@ LUAU_FASTFLAG(DebugLuauFreezeArena)
LUAU_FASTINTVARIABLE(LuauTypeMaximumStringifierLength, 500)
LUAU_FASTINTVARIABLE(LuauTableTypeMaximumStringifierLength, 0)
LUAU_FASTINT(LuauTypeInferRecursionLimit)
LUAU_FASTFLAG(LuauNonCopyableTypeVarFields)
LUAU_FASTFLAG(LuauUnknownAndNeverType)
LUAU_FASTFLAGVARIABLE(LuauDeduceGmatchReturnTypes, false)
LUAU_FASTFLAGVARIABLE(LuauMaybeGenericIntersectionTypes, false)
namespace Luau
{
@ -31,6 +33,9 @@ namespace Luau
std::optional<WithPredicate<TypePackId>> magicFunctionFormat(
TypeChecker& typechecker, const ScopePtr& scope, const AstExprCall& expr, WithPredicate<TypePackId> withPredicate);
static std::optional<WithPredicate<TypePackId>> magicFunctionGmatch(
TypeChecker& typechecker, const ScopePtr& scope, const AstExprCall& expr, WithPredicate<TypePackId> withPredicate);
TypeId follow(TypeId t)
{
return follow(t, [](TypeId t) {
@ -173,8 +178,8 @@ bool maybeString(TypeId ty)
{
ty = follow(ty);
if (isPrim(ty, PrimitiveTypeVar::String) || get<AnyTypeVar>(ty))
return true;
if (isPrim(ty, PrimitiveTypeVar::String) || get<AnyTypeVar>(ty))
return true;
if (auto utv = get<UnionTypeVar>(ty))
return std::any_of(begin(utv), end(utv), maybeString);
@ -194,7 +199,7 @@ bool isOptional(TypeId ty)
ty = follow(ty);
if (get<AnyTypeVar>(ty))
if (get<AnyTypeVar>(ty) || (FFlag::LuauUnknownAndNeverType && get<UnknownTypeVar>(ty)))
return true;
auto utv = get<UnionTypeVar>(ty);
@ -334,6 +339,28 @@ bool isGeneric(TypeId ty)
bool maybeGeneric(TypeId ty)
{
if (FFlag::LuauMaybeGenericIntersectionTypes)
{
ty = follow(ty);
if (get<FreeTypeVar>(ty))
return true;
if (auto ttv = get<TableTypeVar>(ty))
{
// TODO: recurse on table types CLI-39914
(void)ttv;
return true;
}
if (auto itv = get<IntersectionTypeVar>(ty))
{
return std::any_of(begin(itv), end(itv), maybeGeneric);
}
return isGeneric(ty);
}
ty = follow(ty);
if (get<FreeTypeVar>(ty))
return true;
@ -646,20 +673,10 @@ TypeVar& TypeVar::operator=(TypeVariant&& rhs)
TypeVar& TypeVar::operator=(const TypeVar& rhs)
{
if (FFlag::LuauNonCopyableTypeVarFields)
{
LUAU_ASSERT(owningArena == rhs.owningArena);
LUAU_ASSERT(!rhs.persistent);
LUAU_ASSERT(owningArena == rhs.owningArena);
LUAU_ASSERT(!rhs.persistent);
reassign(rhs);
}
else
{
ty = rhs.ty;
persistent = rhs.persistent;
normal = rhs.normal;
owningArena = rhs.owningArena;
}
reassign(rhs);
return *this;
}
@ -676,10 +693,14 @@ static TypeVar threadType_{PrimitiveTypeVar{PrimitiveTypeVar::Thread}, /*persist
static TypeVar trueType_{SingletonTypeVar{BooleanSingleton{true}}, /*persistent*/ true};
static TypeVar falseType_{SingletonTypeVar{BooleanSingleton{false}}, /*persistent*/ true};
static TypeVar anyType_{AnyTypeVar{}, /*persistent*/ true};
static TypeVar unknownType_{UnknownTypeVar{}, /*persistent*/ true};
static TypeVar neverType_{NeverTypeVar{}, /*persistent*/ true};
static TypeVar errorType_{ErrorTypeVar{}, /*persistent*/ true};
static TypePackVar anyTypePack_{VariadicTypePack{&anyType_}, true};
static TypePackVar errorTypePack_{Unifiable::Error{}};
static TypePackVar anyTypePack_{VariadicTypePack{&anyType_}, /*persistent*/ true};
static TypePackVar errorTypePack_{Unifiable::Error{}, /*persistent*/ true};
static TypePackVar neverTypePack_{VariadicTypePack{&neverType_}, /*persistent*/ true};
static TypePackVar uninhabitableTypePack_{TypePack{{&neverType_}, &neverTypePack_}, /*persistent*/ true};
SingletonTypes::SingletonTypes()
: nilType(&nilType_)
@ -690,7 +711,11 @@ SingletonTypes::SingletonTypes()
, trueType(&trueType_)
, falseType(&falseType_)
, anyType(&anyType_)
, unknownType(&unknownType_)
, neverType(&neverType_)
, anyTypePack(&anyTypePack_)
, neverTypePack(&neverTypePack_)
, uninhabitableTypePack(&uninhabitableTypePack_)
, arena(new TypeArena)
{
TypeId stringMetatable = makeStringMetatable();
@ -738,6 +763,7 @@ TypeId SingletonTypes::makeStringMetatable()
const TypeId gsubFunc = makeFunction(*arena, stringType, {}, {}, {stringType, replArgType, optionalNumber}, {}, {stringType, numberType});
const TypeId gmatchFunc =
makeFunction(*arena, stringType, {}, {}, {stringType}, {}, {arena->addType(FunctionTypeVar{emptyPack, stringVariadicList})});
attachMagicFunction(gmatchFunc, magicFunctionGmatch);
TableTypeVar::Props stringLib = {
{"byte", {arena->addType(FunctionTypeVar{arena->addTypePack({stringType, optionalNumber, optionalNumber}), numberVariadicList})}},
@ -911,6 +937,8 @@ const TypeLevel* getLevel(TypeId ty)
return &ttv->level;
else if (auto ftv = get<FunctionTypeVar>(ty))
return &ftv->level;
else if (auto ctv = get<ConstrainedTypeVar>(ty))
return &ctv->level;
else
return nullptr;
}
@ -965,94 +993,19 @@ bool isSubclass(const ClassTypeVar* cls, const ClassTypeVar* parent)
return false;
}
UnionTypeVarIterator::UnionTypeVarIterator(const UnionTypeVar* utv)
const std::vector<TypeId>& getTypes(const UnionTypeVar* utv)
{
LUAU_ASSERT(utv);
if (!utv->options.empty())
stack.push_front({utv, 0});
seen.insert(utv);
return utv->options;
}
UnionTypeVarIterator& UnionTypeVarIterator::operator++()
const std::vector<TypeId>& getTypes(const IntersectionTypeVar* itv)
{
advance();
descend();
return *this;
return itv->parts;
}
UnionTypeVarIterator UnionTypeVarIterator::operator++(int)
const std::vector<TypeId>& getTypes(const ConstrainedTypeVar* ctv)
{
UnionTypeVarIterator copy = *this;
++copy;
return copy;
}
bool UnionTypeVarIterator::operator!=(const UnionTypeVarIterator& rhs)
{
return !(*this == rhs);
}
bool UnionTypeVarIterator::operator==(const UnionTypeVarIterator& rhs)
{
if (!stack.empty() && !rhs.stack.empty())
return stack.front() == rhs.stack.front();
return stack.empty() && rhs.stack.empty();
}
const TypeId& UnionTypeVarIterator::operator*()
{
LUAU_ASSERT(!stack.empty());
descend();
auto [utv, currentIndex] = stack.front();
LUAU_ASSERT(utv);
LUAU_ASSERT(currentIndex < utv->options.size());
const TypeId& ty = utv->options[currentIndex];
LUAU_ASSERT(!get<UnionTypeVar>(follow(ty)));
return ty;
}
void UnionTypeVarIterator::advance()
{
while (!stack.empty())
{
auto& [utv, currentIndex] = stack.front();
++currentIndex;
if (currentIndex >= utv->options.size())
stack.pop_front();
else
break;
}
}
void UnionTypeVarIterator::descend()
{
while (!stack.empty())
{
auto [utv, currentIndex] = stack.front();
if (auto innerUnion = get<UnionTypeVar>(follow(utv->options[currentIndex])))
{
// If we're about to descend into a cyclic UnionTypeVar, we should skip over this.
// Ideally this should never happen, but alas it does from time to time. :(
if (seen.find(innerUnion) != seen.end())
advance();
else
{
seen.insert(innerUnion);
stack.push_front({innerUnion, 0});
}
continue;
}
break;
}
return ctv->parts;
}
UnionTypeVarIterator begin(const UnionTypeVar* utv)
@ -1065,6 +1018,27 @@ UnionTypeVarIterator end(const UnionTypeVar* utv)
return UnionTypeVarIterator{};
}
IntersectionTypeVarIterator begin(const IntersectionTypeVar* itv)
{
return IntersectionTypeVarIterator{itv};
}
IntersectionTypeVarIterator end(const IntersectionTypeVar* itv)
{
return IntersectionTypeVarIterator{};
}
ConstrainedTypeVarIterator begin(const ConstrainedTypeVar* ctv)
{
return ConstrainedTypeVarIterator{ctv};
}
ConstrainedTypeVarIterator end(const ConstrainedTypeVar* ctv)
{
return ConstrainedTypeVarIterator{};
}
static std::vector<TypeId> parseFormatString(TypeChecker& typechecker, const char* data, size_t size)
{
const char* options = "cdiouxXeEfgGqs";
@ -1144,6 +1118,101 @@ std::optional<WithPredicate<TypePackId>> magicFunctionFormat(
return WithPredicate<TypePackId>{arena.addTypePack({typechecker.stringType})};
}
static std::vector<TypeId> parsePatternString(TypeChecker& typechecker, const char* data, size_t size)
{
std::vector<TypeId> result;
int depth = 0;
bool parsingSet = false;
for (size_t i = 0; i < size; ++i)
{
if (data[i] == '%')
{
++i;
if (!parsingSet && i < size && data[i] == 'b')
i += 2;
}
else if (!parsingSet && data[i] == '[')
{
parsingSet = true;
if (i + 1 < size && data[i + 1] == ']')
i += 1;
}
else if (parsingSet && data[i] == ']')
{
parsingSet = false;
}
else if (data[i] == '(')
{
if (parsingSet)
continue;
if (i + 1 < size && data[i + 1] == ')')
{
i++;
result.push_back(typechecker.numberType);
continue;
}
++depth;
result.push_back(typechecker.stringType);
}
else if (data[i] == ')')
{
if (parsingSet)
continue;
--depth;
if (depth < 0)
break;
}
}
if (depth != 0 || parsingSet)
return std::vector<TypeId>();
if (result.empty())
result.push_back(typechecker.stringType);
return result;
}
static std::optional<WithPredicate<TypePackId>> magicFunctionGmatch(
TypeChecker& typechecker, const ScopePtr& scope, const AstExprCall& expr, WithPredicate<TypePackId> withPredicate)
{
if (!FFlag::LuauDeduceGmatchReturnTypes)
return std::nullopt;
auto [paramPack, _predicates] = withPredicate;
const auto& [params, tail] = flatten(paramPack);
if (params.size() != 2)
return std::nullopt;
TypeArena& arena = typechecker.currentModule->internalTypes;
AstExprConstantString* pattern = nullptr;
size_t index = expr.self ? 0 : 1;
if (expr.args.size > index)
pattern = expr.args.data[index]->as<AstExprConstantString>();
if (!pattern)
return std::nullopt;
std::vector<TypeId> returnTypes = parsePatternString(typechecker, pattern->value.data, pattern->value.size);
if (returnTypes.empty())
return std::nullopt;
typechecker.unify(params[0], typechecker.stringType, expr.args.data[0]->location);
const TypePackId emptyPack = arena.addTypePack({});
const TypePackId returnList = arena.addTypePack(returnTypes);
const TypeId iteratorType = arena.addType(FunctionTypeVar{emptyPack, returnList});
return WithPredicate<TypePackId>{arena.addTypePack({iteratorType})};
}
std::vector<TypeId> filterMap(TypeId type, TypeIdPredicate predicate)
{
type = follow(type);

View file

@ -19,6 +19,7 @@ LUAU_FASTFLAG(LuauAutocompleteDynamicLimits)
LUAU_FASTINTVARIABLE(LuauTypeInferLowerBoundsIterationLimit, 2000);
LUAU_FASTFLAG(LuauLowerBoundsCalculation);
LUAU_FASTFLAG(LuauErrorRecoveryType);
LUAU_FASTFLAG(LuauUnknownAndNeverType)
LUAU_FASTFLAG(LuauQuantifyConstrained)
namespace Luau
@ -47,33 +48,6 @@ struct PromoteTypeLevels final : TypeVarOnceVisitor
}
}
// TODO cycle and operator() need to be clipped when FFlagLuauUseVisitRecursionLimit is clipped
template<typename TID>
void cycle(TID)
{
}
template<typename TID, typename T>
bool operator()(TID ty, const T&)
{
return visit(ty);
}
bool operator()(TypeId ty, const FreeTypeVar& ftv)
{
return visit(ty, ftv);
}
bool operator()(TypeId ty, const FunctionTypeVar& ftv)
{
return visit(ty, ftv);
}
bool operator()(TypeId ty, const TableTypeVar& ttv)
{
return visit(ty, ttv);
}
bool operator()(TypePackId tp, const FreeTypePack& ftp)
{
return visit(tp, ftp);
}
bool visit(TypeId ty) override
{
// Type levels of types from other modules are already global, so we don't need to promote anything inside
@ -103,6 +77,15 @@ struct PromoteTypeLevels final : TypeVarOnceVisitor
return true;
}
bool visit(TypeId ty, const ConstrainedTypeVar&) override
{
if (!FFlag::LuauUnknownAndNeverType)
return visit(ty);
promote(ty, log.getMutable<ConstrainedTypeVar>(ty));
return true;
}
bool visit(TypeId ty, const FunctionTypeVar&) override
{
// Type levels of types from other modules are already global, so we don't need to promote anything inside
@ -445,6 +428,14 @@ void Unifier::tryUnify_(TypeId subTy, TypeId superTy, bool isFunctionCall, bool
}
else if (subFree)
{
if (FFlag::LuauUnknownAndNeverType)
{
// Normally, if the subtype is free, it should not be bound to any, unknown, or error types.
// But for bug compatibility, we'll only apply this rule to unknown. Doing this will silence cascading type errors.
if (get<UnknownTypeVar>(superTy))
return;
}
TypeLevel subLevel = subFree->level;
occursCheck(subTy, superTy);
@ -468,7 +459,7 @@ void Unifier::tryUnify_(TypeId subTy, TypeId superTy, bool isFunctionCall, bool
return;
}
if (get<ErrorTypeVar>(superTy) || get<AnyTypeVar>(superTy))
if (get<ErrorTypeVar>(superTy) || get<AnyTypeVar>(superTy) || get<UnknownTypeVar>(superTy))
return tryUnifyWithAny(subTy, superTy);
if (get<AnyTypeVar>(subTy))
@ -485,6 +476,9 @@ void Unifier::tryUnify_(TypeId subTy, TypeId superTy, bool isFunctionCall, bool
if (get<ErrorTypeVar>(subTy))
return tryUnifyWithAny(superTy, subTy);
if (get<NeverTypeVar>(subTy))
return tryUnifyWithAny(superTy, subTy);
auto& cache = sharedState.cachedUnify;
// What if the types are immutable and we proved their relation before
@ -1862,6 +1856,7 @@ static void tryUnifyWithAny(std::vector<TypeId>& queue, Unifier& state, DenseHas
if (state.log.getMutable<FreeTypeVar>(ty))
{
// TODO: Only bind if the anyType isn't any, unknown, or error (?)
state.log.replace(ty, BoundTypeVar{anyType});
}
else if (auto fun = state.log.getMutable<FunctionTypeVar>(ty))
@ -1901,22 +1896,27 @@ static void tryUnifyWithAny(std::vector<TypeId>& queue, Unifier& state, DenseHas
void Unifier::tryUnifyWithAny(TypeId subTy, TypeId anyTy)
{
LUAU_ASSERT(get<AnyTypeVar>(anyTy) || get<ErrorTypeVar>(anyTy));
LUAU_ASSERT(get<AnyTypeVar>(anyTy) || get<ErrorTypeVar>(anyTy) || get<UnknownTypeVar>(anyTy) || get<NeverTypeVar>(anyTy));
// These types are not visited in general loop below
if (get<PrimitiveTypeVar>(subTy) || get<AnyTypeVar>(subTy) || get<ClassTypeVar>(subTy))
return;
const TypePackId anyTypePack = types->addTypePack(TypePackVar{VariadicTypePack{getSingletonTypes().anyType}});
const TypePackId anyTP = get<AnyTypeVar>(anyTy) ? anyTypePack : types->addTypePack(TypePackVar{Unifiable::Error{}});
TypePackId anyTp;
if (FFlag::LuauUnknownAndNeverType)
anyTp = types->addTypePack(TypePackVar{VariadicTypePack{anyTy}});
else
{
const TypePackId anyTypePack = types->addTypePack(TypePackVar{VariadicTypePack{getSingletonTypes().anyType}});
anyTp = get<AnyTypeVar>(anyTy) ? anyTypePack : types->addTypePack(TypePackVar{Unifiable::Error{}});
}
std::vector<TypeId> queue = {subTy};
sharedState.tempSeenTy.clear();
sharedState.tempSeenTp.clear();
Luau::tryUnifyWithAny(queue, *this, sharedState.tempSeenTy, sharedState.tempSeenTp, types, getSingletonTypes().anyType, anyTP);
Luau::tryUnifyWithAny(queue, *this, sharedState.tempSeenTy, sharedState.tempSeenTp, types, FFlag::LuauUnknownAndNeverType ? anyTy : getSingletonTypes().anyType, anyTp);
}
void Unifier::tryUnifyWithAny(TypePackId subTy, TypePackId anyTp)

View file

@ -15,7 +15,6 @@ LUAU_FASTINTVARIABLE(LuauRecursionLimit, 1000)
LUAU_FASTINTVARIABLE(LuauParseErrorLimit, 100)
LUAU_FASTFLAGVARIABLE(LuauParserFunctionKeywordAsTypeHelp, false)
LUAU_FASTFLAGVARIABLE(LuauReturnTypeTokenConfusion, false)
LUAU_FASTFLAGVARIABLE(LuauFixNamedFunctionParse, false)
LUAU_DYNAMIC_FASTFLAGVARIABLE(LuaReportParseWrongNamedType, false)
@ -1134,10 +1133,9 @@ AstTypePack* Parser::parseTypeList(TempVector<AstType*>& result, TempVector<std:
std::optional<AstTypeList> Parser::parseOptionalReturnTypeAnnotation()
{
if (options.allowTypeAnnotations &&
(lexer.current().type == ':' || (FFlag::LuauReturnTypeTokenConfusion && lexer.current().type == Lexeme::SkinnyArrow)))
if (options.allowTypeAnnotations && (lexer.current().type == ':' || lexer.current().type == Lexeme::SkinnyArrow))
{
if (FFlag::LuauReturnTypeTokenConfusion && lexer.current().type == Lexeme::SkinnyArrow)
if (lexer.current().type == Lexeme::SkinnyArrow)
report(lexer.current().location, "Function return type annotations are written after ':' instead of '->'");
nextLexeme();
@ -1373,12 +1371,10 @@ AstTypeOrPack Parser::parseFunctionTypeAnnotation(bool allowPack)
if (FFlag::LuauFixNamedFunctionParse && !names.empty())
forceFunctionType = true;
bool returnTypeIntroducer =
FFlag::LuauReturnTypeTokenConfusion ? lexer.current().type == Lexeme::SkinnyArrow || lexer.current().type == ':' : false;
bool returnTypeIntroducer = lexer.current().type == Lexeme::SkinnyArrow || lexer.current().type == ':';
// Not a function at all. Just a parenthesized type. Or maybe a type pack with a single element
if (params.size() == 1 && !varargAnnotation && !forceFunctionType &&
(FFlag::LuauReturnTypeTokenConfusion ? !returnTypeIntroducer : lexer.current().type != Lexeme::SkinnyArrow))
if (params.size() == 1 && !varargAnnotation && !forceFunctionType && !returnTypeIntroducer)
{
if (DFFlag::LuaReportParseWrongNamedType && !names.empty())
lua_telemetry_parsed_named_non_function_type = true;
@ -1389,8 +1385,7 @@ AstTypeOrPack Parser::parseFunctionTypeAnnotation(bool allowPack)
return {params[0], {}};
}
if ((FFlag::LuauReturnTypeTokenConfusion ? !returnTypeIntroducer : lexer.current().type != Lexeme::SkinnyArrow) && !forceFunctionType &&
allowPack)
if (!forceFunctionType && !returnTypeIntroducer && allowPack)
{
if (DFFlag::LuaReportParseWrongNamedType && !names.empty())
lua_telemetry_parsed_named_non_function_type = true;
@ -1409,7 +1404,7 @@ AstType* Parser::parseFunctionTypeAnnotationTail(const Lexeme& begin, AstArray<A
{
incrementRecursionCounter("type annotation");
if (FFlag::LuauReturnTypeTokenConfusion && lexer.current().type == ':')
if (lexer.current().type == ':')
{
report(lexer.current().location, "Return types in function type annotations are written after '->' instead of ':'");
lexer.next();

View file

@ -8,6 +8,10 @@
#include "FileUtils.h"
#ifdef CALLGRIND
#include <valgrind/callgrind.h>
#endif
LUAU_FASTFLAG(DebugLuauTimeTracing)
LUAU_FASTFLAG(LuauTypeMismatchModuleNameResolution)
@ -112,6 +116,7 @@ static void displayHelp(const char* argv0)
printf("Available options:\n");
printf(" --formatter=plain: report analysis errors in Luacheck-compatible format\n");
printf(" --formatter=gnu: report analysis errors in GNU-compatible format\n");
printf(" --mode=strict: default to strict mode when typechecking\n");
printf(" --timetrace: record compiler time tracing information into trace.json\n");
}
@ -178,9 +183,9 @@ struct CliConfigResolver : Luau::ConfigResolver
mutable std::unordered_map<std::string, Luau::Config> configCache;
mutable std::vector<std::pair<std::string, std::string>> configErrors;
CliConfigResolver()
CliConfigResolver(Luau::Mode mode)
{
defaultConfig.mode = Luau::Mode::Nonstrict;
defaultConfig.mode = mode;
}
const Luau::Config& getConfig(const Luau::ModuleName& name) const override
@ -229,6 +234,7 @@ int main(int argc, char** argv)
}
ReportFormat format = ReportFormat::Default;
Luau::Mode mode = Luau::Mode::Nonstrict;
bool annotate = false;
for (int i = 1; i < argc; ++i)
@ -240,6 +246,8 @@ int main(int argc, char** argv)
format = ReportFormat::Luacheck;
else if (strcmp(argv[i], "--formatter=gnu") == 0)
format = ReportFormat::Gnu;
else if (strcmp(argv[i], "--mode=strict") == 0)
mode = Luau::Mode::Strict;
else if (strcmp(argv[i], "--annotate") == 0)
annotate = true;
else if (strcmp(argv[i], "--timetrace") == 0)
@ -258,12 +266,16 @@ int main(int argc, char** argv)
frontendOptions.retainFullTypeGraphs = annotate;
CliFileResolver fileResolver;
CliConfigResolver configResolver;
CliConfigResolver configResolver(mode);
Luau::Frontend frontend(&fileResolver, &configResolver, frontendOptions);
Luau::registerBuiltinTypes(frontend.typeChecker);
Luau::freeze(frontend.typeChecker.globalTypes);
#ifdef CALLGRIND
CALLGRIND_ZERO_STATS;
#endif
std::vector<std::string> files = getSourceFiles(argc, argv);
int failed = 0;

View file

@ -21,6 +21,10 @@
#include <fcntl.h>
#endif
#ifdef CALLGRIND
#include <valgrind/callgrind.h>
#endif
#include <locale.h>
LUAU_FASTFLAG(DebugLuauTimeTracing)
@ -166,6 +170,36 @@ static int lua_collectgarbage(lua_State* L)
luaL_error(L, "collectgarbage must be called with 'count' or 'collect'");
}
#ifdef CALLGRIND
static int lua_callgrind(lua_State* L)
{
const char* option = luaL_checkstring(L, 1);
if (strcmp(option, "running") == 0)
{
int r = RUNNING_ON_VALGRIND;
lua_pushboolean(L, r);
return 1;
}
if (strcmp(option, "zero") == 0)
{
CALLGRIND_ZERO_STATS;
return 0;
}
if (strcmp(option, "dump") == 0)
{
const char* name = luaL_checkstring(L, 2);
CALLGRIND_DUMP_STATS_AT(name);
return 0;
}
luaL_error(L, "callgrind must be called with one of 'running', 'zero', 'dump'");
}
#endif
void setupState(lua_State* L)
{
luaL_openlibs(L);
@ -174,6 +208,9 @@ void setupState(lua_State* L)
{"loadstring", lua_loadstring},
{"require", lua_require},
{"collectgarbage", lua_collectgarbage},
#ifdef CALLGRIND
{"callgrind", lua_callgrind},
#endif
{NULL, NULL},
};

View file

@ -58,6 +58,9 @@ public:
void jmp(Label& label);
void jmp(OperandX64 op);
void call(Label& label);
void call(OperandX64 op);
// AVX
void vaddpd(OperandX64 dst, OperandX64 src1, OperandX64 src2);
void vaddps(OperandX64 dst, OperandX64 src1, OperandX64 src2);

View file

@ -286,11 +286,34 @@ void AssemblyBuilderX64::jmp(OperandX64 op)
if (logText)
log("jmp", op);
placeRex(op);
place(0xff);
placeModRegMem(op, 4);
commit();
}
void AssemblyBuilderX64::call(Label& label)
{
place(0xe8);
placeLabel(label);
if (logText)
log("call", label);
commit();
}
void AssemblyBuilderX64::call(OperandX64 op)
{
if (logText)
log("call", op);
placeRex(op);
place(0xff);
placeModRegMem(op, 2);
commit();
}
void AssemblyBuilderX64::vaddpd(OperandX64 dst, OperandX64 src1, OperandX64 src2)
{
placeAvx("vaddpd", dst, src1, src2, 0x58, false, AVX_0F, AVX_66);

View file

@ -93,6 +93,10 @@ ifeq ($(config),fuzz)
LDFLAGS+=-fsanitize=address,fuzzer
endif
ifneq ($(CALLGRIND),)
CXXFLAGS+=-DCALLGRIND=$(CALLGRIND)
endif
# target-specific flags
$(AST_OBJECTS): CXXFLAGS+=-std=c++17 -ICommon/include -IAst/include
$(COMPILER_OBJECTS): CXXFLAGS+=-std=c++17 -ICompiler/include -ICommon/include -IAst/include

View file

@ -247,12 +247,15 @@ if(TARGET Luau.UnitTest)
tests/IostreamOptional.h
tests/ScopedFlags.h
tests/Fixture.cpp
tests/AssemblyBuilderX64.test.cpp
tests/AstQuery.test.cpp
tests/AstVisitor.test.cpp
tests/Autocomplete.test.cpp
tests/BuiltinDefinitions.test.cpp
tests/Compiler.test.cpp
tests/Config.test.cpp
tests/ConstraintGraphBuilder.test.cpp
tests/ConstraintSolver.test.cpp
tests/CostModel.test.cpp
tests/Error.test.cpp
tests/Frontend.test.cpp
@ -262,8 +265,7 @@ if(TARGET Luau.UnitTest)
tests/Module.test.cpp
tests/NonstrictMode.test.cpp
tests/Normalize.test.cpp
tests/ConstraintGraphBuilder.test.cpp
tests/ConstraintSolver.test.cpp
tests/NotNull.test.cpp
tests/Parser.test.cpp
tests/RequireTracer.test.cpp
tests/RuntimeLimits.test.cpp
@ -295,11 +297,11 @@ if(TARGET Luau.UnitTest)
tests/TypeInfer.tryUnify.test.cpp
tests/TypeInfer.typePacks.cpp
tests/TypeInfer.unionTypes.test.cpp
tests/TypeInfer.unknownnever.test.cpp
tests/TypePack.test.cpp
tests/TypeVar.test.cpp
tests/Variant.test.cpp
tests/VisitTypeVar.test.cpp
tests/AssemblyBuilderX64.test.cpp
tests/main.cpp)
endif()

View file

@ -108,9 +108,9 @@ static LuaNode* hashvec(const Table* t, const float* v)
memcpy(i, v, sizeof(i));
// convert -0 to 0 to make sure they hash to the same value
i[0] = (i[0] == 0x8000000) ? 0 : i[0];
i[1] = (i[1] == 0x8000000) ? 0 : i[1];
i[2] = (i[2] == 0x8000000) ? 0 : i[2];
i[0] = (i[0] == 0x80000000) ? 0 : i[0];
i[1] = (i[1] == 0x80000000) ? 0 : i[1];
i[2] = (i[2] == 0x80000000) ? 0 : i[2];
// scramble bits to make sure that integer coordinates have entropy in lower bits
i[0] ^= i[0] >> 17;
@ -121,7 +121,7 @@ static LuaNode* hashvec(const Table* t, const float* v)
unsigned int h = (i[0] * 73856093) ^ (i[1] * 19349663) ^ (i[2] * 83492791);
#if LUA_VECTOR_SIZE == 4
i[3] = (i[3] == 0x8000000) ? 0 : i[3];
i[3] = (i[3] == 0x80000000) ? 0 : i[3];
i[3] ^= i[3] >> 17;
h ^= i[3] * 39916801;
#endif

View file

@ -640,20 +640,16 @@ static void luau_execute(lua_State* L)
VM_PATCH_C(pc - 2, L->cachedslot);
VM_NEXT();
}
else
{
// slow-path, may invoke Lua calls via __index metamethod
VM_PROTECT(luaV_gettable(L, rb, kv, ra));
VM_NEXT();
}
}
else
{
// slow-path, may invoke Lua calls via __index metamethod
VM_PROTECT(luaV_gettable(L, rb, kv, ra));
VM_NEXT();
// fall through to slow path
}
// fall through to slow path
}
// slow-path, may invoke Lua calls via __index metamethod
VM_PROTECT(luaV_gettable(L, rb, kv, ra));
VM_NEXT();
}
VM_CASE(LOP_SETTABLEKS)
@ -753,19 +749,13 @@ static void luau_execute(lua_State* L)
setobj2s(L, ra, &h->array[unsigned(index - 1)]);
VM_NEXT();
}
else
{
// slow-path: handles out of bounds array lookups and non-integer numeric keys
VM_PROTECT(luaV_gettable(L, rb, rc, ra));
VM_NEXT();
}
}
else
{
// slow-path: handles non-array table lookup as well as __index MT calls
VM_PROTECT(luaV_gettable(L, rb, rc, ra));
VM_NEXT();
// fall through to slow path
}
// slow-path: handles out of bounds array lookups, non-integer numeric keys, non-array table lookup, __index MT calls
VM_PROTECT(luaV_gettable(L, rb, rc, ra));
VM_NEXT();
}
VM_CASE(LOP_SETTABLE)
@ -790,19 +780,13 @@ static void luau_execute(lua_State* L)
luaC_barriert(L, h, ra);
VM_NEXT();
}
else
{
// slow-path: handles out of bounds array assignments and non-integer numeric keys
VM_PROTECT(luaV_settable(L, rb, rc, ra));
VM_NEXT();
}
}
else
{
// slow-path: handles non-array table access as well as __newindex MT calls
VM_PROTECT(luaV_settable(L, rb, rc, ra));
VM_NEXT();
// fall through to slow path
}
// slow-path: handles out of bounds array assignments, non-integer numeric keys, non-array table access, __newindex MT calls
VM_PROTECT(luaV_settable(L, rb, rc, ra));
VM_NEXT();
}
VM_CASE(LOP_GETTABLEN)
@ -822,6 +806,8 @@ static void luau_execute(lua_State* L)
setobj2s(L, ra, &h->array[c]);
VM_NEXT();
}
// fall through to slow path
}
// slow-path: handles out of bounds array lookups
@ -849,6 +835,8 @@ static void luau_execute(lua_State* L)
luaC_barriert(L, h, ra);
VM_NEXT();
}
// fall through to slow path
}
// slow-path: handles out of bounds array lookups
@ -2176,8 +2164,10 @@ static void luau_execute(lua_State* L)
if (!ttisnumber(ra + 0) || !ttisnumber(ra + 1) || !ttisnumber(ra + 2))
{
// slow-path: can convert arguments to numbers and trigger Lua errors
// Note: this doesn't reallocate stack so we don't need to recompute ra
VM_PROTECT(luau_prepareFORN(L, ra + 0, ra + 1, ra + 2));
// Note: this doesn't reallocate stack so we don't need to recompute ra/base
VM_PROTECT_PC();
luau_prepareFORN(L, ra + 0, ra + 1, ra + 2);
}
double limit = nvalue(ra + 0);

View file

@ -40,6 +40,7 @@ argumentParser.add_argument('--results', dest='results',type=str,nargs='*',help=
argumentParser.add_argument('--run-test', action='store', default=None, help='Regex test filter')
argumentParser.add_argument('--extra-loops', action='store',type=int,default=0, help='Amount of times to loop over one test (one test already performs multiple runs)')
argumentParser.add_argument('--filename', action='store',type=str,default='bench', help='File name for graph and results file')
argumentParser.add_argument('--callgrind', dest='callgrind',action='store_const',const=1,default=0,help='Use callgrind to run benchmarks')
if matplotlib != None:
argumentParser.add_argument('--absolute', dest='absolute',action='store_const',const=1,default=0,help='Display absolute values instead of relative (enabled by default when benchmarking a single VM)')
@ -55,6 +56,9 @@ argumentParser.add_argument('--no-print-influx-debugging', action='store_false',
argumentParser.add_argument('--no-print-final-summary', action='store_false', dest='print_final_summary', help="Don't print a table summarizing the results after all tests are run")
# Assume 2.5 IPC on a 4 GHz CPU; this is obviously incorrect but it allows us to display simulated instruction counts using regular time units
CALLGRIND_INSN_PER_SEC = 2.5 * 4e9
def arrayRange(count):
result = []
@ -71,6 +75,21 @@ def arrayRangeOffset(count, offset):
return result
def getCallgrindOutput(lines):
result = []
name = None
for l in lines:
if l.startswith("desc: Trigger: Client Request: "):
name = l[31:].strip()
elif l.startswith("summary: ") and name != None:
insn = int(l[9:])
# Note: we only run each bench once under callgrind so we only report a single time per run; callgrind instruction count variance is ~0.01% so it might as well be zero
result += "|><|" + name + "|><|" + str(insn / CALLGRIND_INSN_PER_SEC * 1000.0) + "||_||"
name = None
return "".join(result)
def getVmOutput(cmd):
if os.name == "nt":
try:
@ -79,6 +98,16 @@ def getVmOutput(cmd):
exit(1)
except:
return ""
elif arguments.callgrind:
try:
subprocess.check_call("valgrind --tool=callgrind --callgrind-out-file=callgrind.out --combine-dumps=yes --dump-line=no " + cmd, shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, cwd=scriptdir)
path = os.path.join(scriptdir, "callgrind.out")
with open(path, "r") as file:
lines = file.readlines()
os.unlink(path)
return getCallgrindOutput(lines)
except:
return ""
else:
with subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, cwd=scriptdir) as p:
# Try to lock to a single processor
@ -375,12 +404,12 @@ def analyzeResult(subdir, main, comparisons):
continue
pooledStdDev = math.sqrt((main.unbiasedEst + compare.unbiasedEst) / 2)
if main.count > 1 and stats:
pooledStdDev = math.sqrt((main.unbiasedEst + compare.unbiasedEst) / 2)
tStat = abs(main.avg - compare.avg) / (pooledStdDev * math.sqrt(2 / main.count))
degreesOfFreedom = 2 * main.count - 2
tStat = abs(main.avg - compare.avg) / (pooledStdDev * math.sqrt(2 / main.count))
degreesOfFreedom = 2 * main.count - 2
if stats:
# Two-tailed distribution with 95% conf.
tCritical = stats.t.ppf(1 - 0.05 / 2, degreesOfFreedom)

View file

@ -5,6 +5,16 @@ bench.runs = 20
bench.extraRuns = 4
function bench.runCode(f, description)
-- Under Callgrind, run the test only once and measure just the execution cost
if callgrind and callgrind("running") then
if collectgarbage then collectgarbage() end
callgrind("zero")
f() -- unfortunately we can't easily separate setup cost from runtime cost in f unless it calls callgrind()
callgrind("dump", description)
return
end
local timeTable = {}
for i = 1,bench.runs + bench.extraRuns do

View file

@ -0,0 +1,961 @@
-- This file is part of the Roblox luau-polyfill repository and is licensed under MIT License; see LICENSE.txt for details
-- #region Array
-- Array related
local Array = {}
local Object = {}
local Map = {}
type Array<T> = { [number]: T }
type callbackFn<K, V> = (element: V, key: K, map: Map<K, V>) -> ()
type callbackFnWithThisArg<K, V> = (thisArg: Object, value: V, key: K, map: Map<K, V>) -> ()
type Map<K, V> = {
size: number,
-- method definitions
set: (self: Map<K, V>, K, V) -> Map<K, V>,
get: (self: Map<K, V>, K) -> V | nil,
clear: (self: Map<K, V>) -> (),
delete: (self: Map<K, V>, K) -> boolean,
forEach: (self: Map<K, V>, callback: callbackFn<K, V> | callbackFnWithThisArg<K, V>, thisArg: Object?) -> (),
has: (self: Map<K, V>, K) -> boolean,
keys: (self: Map<K, V>) -> Array<K>,
values: (self: Map<K, V>) -> Array<V>,
entries: (self: Map<K, V>) -> Array<Tuple<K, V>>,
ipairs: (self: Map<K, V>) -> any,
[K]: V,
_map: { [K]: V },
_array: { [number]: K },
}
type mapFn<T, U> = (element: T, index: number) -> U
type mapFnWithThisArg<T, U> = (thisArg: any, element: T, index: number) -> U
type Object = { [string]: any }
type Table<T, V> = { [T]: V }
type Tuple<T, V> = Array<T | V>
local Set = {}
-- #region Array
function Array.isArray(value: any): boolean
if typeof(value) ~= "table" then
return false
end
if next(value) == nil then
-- an empty table is an empty array
return true
end
local length = #value
if length == 0 then
return false
end
local count = 0
local sum = 0
for key in pairs(value) do
if typeof(key) ~= "number" then
return false
end
if key % 1 ~= 0 or key < 1 then
return false
end
count += 1
sum += key
end
return sum == (count * (count + 1) / 2)
end
function Array.from<T, U>(
value: string | Array<T> | Object,
mapFn: (mapFn<T, U> | mapFnWithThisArg<T, U>)?,
thisArg: Object?
): Array<U>
if value == nil then
error("cannot create array from a nil value")
end
local valueType = typeof(value)
local array = {}
if valueType == "table" and Array.isArray(value) then
if mapFn then
for i = 1, #(value :: Array<T>) do
if thisArg ~= nil then
array[i] = (mapFn :: mapFnWithThisArg<T, U>)(thisArg, (value :: Array<T>)[i], i)
else
array[i] = (mapFn :: mapFn<T, U>)((value :: Array<T>)[i], i)
end
end
else
for i = 1, #(value :: Array<T>) do
array[i] = (value :: Array<any>)[i]
end
end
elseif instanceOf(value, Set) then
if mapFn then
for i, v in (value :: any):ipairs() do
if thisArg ~= nil then
array[i] = (mapFn :: mapFnWithThisArg<T, U>)(thisArg, v, i)
else
array[i] = (mapFn :: mapFn<T, U>)(v, i)
end
end
else
for i, v in (value :: any):ipairs() do
array[i] = v
end
end
elseif instanceOf(value, Map) then
if mapFn then
for i, v in (value :: any):ipairs() do
if thisArg ~= nil then
array[i] = (mapFn :: mapFnWithThisArg<T, U>)(thisArg, v, i)
else
array[i] = (mapFn :: mapFn<T, U>)(v, i)
end
end
else
for i, v in (value :: any):ipairs() do
array[i] = v
end
end
elseif valueType == "string" then
if mapFn then
for i = 1, (value :: string):len() do
if thisArg ~= nil then
array[i] = (mapFn :: mapFnWithThisArg<T, U>)(thisArg, (value :: any):sub(i, i), i)
else
array[i] = (mapFn :: mapFn<T, U>)((value :: any):sub(i, i), i)
end
end
else
for i = 1, (value :: string):len() do
array[i] = (value :: any):sub(i, i)
end
end
end
return array
end
type callbackFnArrayMap<T, U> = (element: T, index: number, array: Array<T>) -> U
type callbackFnWithThisArgArrayMap<T, U, V> = (thisArg: V, element: T, index: number, array: Array<T>) -> U
-- Implements Javascript's `Array.prototype.map` as defined below
-- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map
function Array.map<T, U, V>(
t: Array<T>,
callback: callbackFnArrayMap<T, U> | callbackFnWithThisArgArrayMap<T, U, V>,
thisArg: V?
): Array<U>
if typeof(t) ~= "table" then
error(string.format("Array.map called on %s", typeof(t)))
end
if typeof(callback) ~= "function" then
error("callback is not a function")
end
local len = #t
local A = {}
local k = 1
while k <= len do
local kValue = t[k]
if kValue ~= nil then
local mappedValue
if thisArg ~= nil then
mappedValue = (callback :: callbackFnWithThisArgArrayMap<T, U, V>)(thisArg, kValue, k, t)
else
mappedValue = (callback :: callbackFnArrayMap<T, U>)(kValue, k, t)
end
A[k] = mappedValue
end
k += 1
end
return A
end
type Function = (any, any, number, any) -> any
-- Implements Javascript's `Array.prototype.reduce` as defined below
-- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce
function Array.reduce<T>(array: Array<T>, callback: Function, initialValue: any?): any
if typeof(array) ~= "table" then
error(string.format("Array.reduce called on %s", typeof(array)))
end
if typeof(callback) ~= "function" then
error("callback is not a function")
end
local length = #array
local value
local initial = 1
if initialValue ~= nil then
value = initialValue
else
initial = 2
if length == 0 then
error("reduce of empty array with no initial value")
end
value = array[1]
end
for i = initial, length do
value = callback(value, array[i], i, array)
end
return value
end
type callbackFnArrayForEach<T> = (element: T, index: number, array: Array<T>) -> ()
type callbackFnWithThisArgArrayForEach<T, U> = (thisArg: U, element: T, index: number, array: Array<T>) -> ()
-- Implements Javascript's `Array.prototype.forEach` as defined below
-- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach
function Array.forEach<T, U>(
t: Array<T>,
callback: callbackFnArrayForEach<T> | callbackFnWithThisArgArrayForEach<T, U>,
thisArg: U?
): ()
if typeof(t) ~= "table" then
error(string.format("Array.forEach called on %s", typeof(t)))
end
if typeof(callback) ~= "function" then
error("callback is not a function")
end
local len = #t
local k = 1
while k <= len do
local kValue = t[k]
if thisArg ~= nil then
(callback :: callbackFnWithThisArgArrayForEach<T, U>)(thisArg, kValue, k, t)
else
(callback :: callbackFnArrayForEach<T>)(kValue, k, t)
end
if #t < len then
-- don't iterate on removed items, don't iterate more than original length
len = #t
end
k += 1
end
end
-- #endregion
-- #region Set
Set.__index = Set
type callbackFnSet<T> = (value: T, key: T, set: Set<T>) -> ()
type callbackFnWithThisArgSet<T> = (thisArg: Object, value: T, key: T, set: Set<T>) -> ()
export type Set<T> = {
size: number,
-- method definitions
add: (self: Set<T>, T) -> Set<T>,
clear: (self: Set<T>) -> (),
delete: (self: Set<T>, T) -> boolean,
forEach: (self: Set<T>, callback: callbackFnSet<T> | callbackFnWithThisArgSet<T>, thisArg: Object?) -> (),
has: (self: Set<T>, T) -> boolean,
ipairs: (self: Set<T>) -> any,
}
type Iterable = { ipairs: (any) -> any }
function Set.new<T>(iterable: Array<T> | Set<T> | Iterable | string | nil): Set<T>
local array = {}
local map = {}
if iterable ~= nil then
local arrayIterable: Array<any>
-- ROBLOX TODO: remove type casting from (iterable :: any).ipairs in next release
if typeof(iterable) == "table" then
if Array.isArray(iterable) then
arrayIterable = Array.from(iterable :: Array<any>)
elseif typeof((iterable :: Iterable).ipairs) == "function" then
-- handle in loop below
elseif _G.__DEV__ then
error("cannot create array from an object-like table")
end
elseif typeof(iterable) == "string" then
arrayIterable = Array.from(iterable :: string)
else
error(("cannot create array from value of type `%s`"):format(typeof(iterable)))
end
if arrayIterable then
for _, element in ipairs(arrayIterable) do
if not map[element] then
map[element] = true
table.insert(array, element)
end
end
elseif typeof(iterable) == "table" and typeof((iterable :: Iterable).ipairs) == "function" then
for _, element in (iterable :: Iterable):ipairs() do
if not map[element] then
map[element] = true
table.insert(array, element)
end
end
end
end
return (setmetatable({
size = #array,
_map = map,
_array = array,
}, Set) :: any) :: Set<T>
end
function Set:add(value)
if not self._map[value] then
-- Luau FIXME: analyze should know self is Set<T> which includes size as a number
self.size = self.size :: number + 1
self._map[value] = true
table.insert(self._array, value)
end
return self
end
function Set:clear()
self.size = 0
table.clear(self._map)
table.clear(self._array)
end
function Set:delete(value): boolean
if not self._map[value] then
return false
end
-- Luau FIXME: analyze should know self is Map<K, V> which includes size as a number
self.size = self.size :: number - 1
self._map[value] = nil
local index = table.find(self._array, value)
if index then
table.remove(self._array, index)
end
return true
end
-- Implements Javascript's `Map.prototype.forEach` as defined below
-- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/forEach
function Set:forEach<T>(callback: callbackFnSet<T> | callbackFnWithThisArgSet<T>, thisArg: Object?): ()
if typeof(callback) ~= "function" then
error("callback is not a function")
end
return Array.forEach(self._array, function(value: T)
if thisArg ~= nil then
(callback :: callbackFnWithThisArgSet<T>)(thisArg, value, value, self)
else
(callback :: callbackFnSet<T>)(value, value, self)
end
end)
end
function Set:has(value): boolean
return self._map[value] ~= nil
end
function Set:ipairs()
return ipairs(self._array)
end
-- #endregion Set
-- #region Object
function Object.entries(value: string | Object | Array<any>): Array<any>
assert(value :: any ~= nil, "cannot get entries from a nil value")
local valueType = typeof(value)
local entries: Array<Tuple<string, any>> = {}
if valueType == "table" then
for key, keyValue in pairs(value :: Object) do
-- Luau FIXME: Luau should see entries as Array<any>, given object is [string]: any, but it sees it as Array<Array<string>> despite all the manual annotation
table.insert(entries, { key :: string, keyValue :: any })
end
elseif valueType == "string" then
for i = 1, string.len(value :: string) do
entries[i] = { tostring(i), string.sub(value :: string, i, i) }
end
end
return entries
end
-- #endregion
-- #region instanceOf
-- ROBLOX note: Typed tbl as any to work with strict type analyze
-- polyfill for https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/instanceof
function instanceOf(tbl: any, class)
assert(typeof(class) == "table", "Received a non-table as the second argument for instanceof")
if typeof(tbl) ~= "table" then
return false
end
local ok, hasNew = pcall(function()
return class.new ~= nil and tbl.new == class.new
end)
if ok and hasNew then
return true
end
local seen = { tbl = true }
while tbl and typeof(tbl) == "table" do
tbl = getmetatable(tbl)
if typeof(tbl) == "table" then
tbl = tbl.__index
if tbl == class then
return true
end
end
-- if we still have a valid table then check against seen
if typeof(tbl) == "table" then
if seen[tbl] then
return false
end
seen[tbl] = true
end
end
return false
end
-- #endregion
function Map.new<K, V>(iterable: Array<Array<any>>?): Map<K, V>
local array = {}
local map = {}
if iterable ~= nil then
local arrayFromIterable
local iterableType = typeof(iterable)
if iterableType == "table" then
if #iterable > 0 and typeof(iterable[1]) ~= "table" then
error("cannot create Map from {K, V} form, it must be { {K, V}... }")
end
arrayFromIterable = Array.from(iterable)
else
error(("cannot create array from value of type `%s`"):format(iterableType))
end
for _, entry in ipairs(arrayFromIterable) do
local key = entry[1]
if _G.__DEV__ then
if key == nil then
error("cannot create Map from a table that isn't an array.")
end
end
local val = entry[2]
-- only add to array if new
if map[key] == nil then
table.insert(array, key)
end
-- always assign
map[key] = val
end
end
return (setmetatable({
size = #array,
_map = map,
_array = array,
}, Map) :: any) :: Map<K, V>
end
function Map:set<K, V>(key: K, value: V): Map<K, V>
-- preserve initial insertion order
if self._map[key] == nil then
-- Luau FIXME: analyze should know self is Map<K, V> which includes size as a number
self.size = self.size :: number + 1
table.insert(self._array, key)
end
-- always update value
self._map[key] = value
return self
end
function Map:get(key)
return self._map[key]
end
function Map:clear()
local table_: any = table
self.size = 0
table_.clear(self._map)
table_.clear(self._array)
end
function Map:delete(key): boolean
if self._map[key] == nil then
return false
end
-- Luau FIXME: analyze should know self is Map<K, V> which includes size as a number
self.size = self.size :: number - 1
self._map[key] = nil
local index = table.find(self._array, key)
if index then
table.remove(self._array, index)
end
return true
end
-- Implements Javascript's `Map.prototype.forEach` as defined below
-- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/forEach
function Map:forEach<K, V>(callback: callbackFn<K, V> | callbackFnWithThisArg<K, V>, thisArg: Object?): ()
if typeof(callback) ~= "function" then
error("callback is not a function")
end
return Array.forEach(self._array, function(key: K)
local value: V = self._map[key] :: V
if thisArg ~= nil then
(callback :: callbackFnWithThisArg<K, V>)(thisArg, value, key, self)
else
(callback :: callbackFn<K, V>)(value, key, self)
end
end)
end
function Map:has(key): boolean
return self._map[key] ~= nil
end
function Map:keys()
return self._array
end
function Map:values()
return Array.map(self._array, function(key)
return self._map[key]
end)
end
function Map:entries()
return Array.map(self._array, function(key)
return { key, self._map[key] }
end)
end
function Map:ipairs()
return ipairs(self:entries())
end
function Map.__index(self, key)
local mapProp = rawget(Map, key)
if mapProp ~= nil then
return mapProp
end
return Map.get(self, key)
end
function Map.__newindex(table_, key, value)
table_:set(key, value)
end
local function coerceToMap(mapLike: Map<any, any> | Table<any, any>): Map<any, any>
return instanceOf(mapLike, Map) and mapLike :: Map<any, any> -- ROBLOX: order is preservered
or Map.new(Object.entries(mapLike)) -- ROBLOX: order is not preserved
end
-- local function coerceToTable(mapLike: Map<any, any> | Table<any, any>): Table<any, any>
-- if not instanceOf(mapLike, Map) then
-- return mapLike
-- end
-- -- create table from map
-- return Array.reduce(mapLike:entries(), function(tbl, entry)
-- tbl[entry[1]] = entry[2]
-- return tbl
-- end, {})
-- end
-- #region Tests to verify it works as expected
local function it(description: string, fn: () -> ())
local ok, result = pcall(fn)
if not ok then
error("Failed test: " .. description .. "\n" .. result)
end
end
local AN_ITEM = "bar"
local ANOTHER_ITEM = "baz"
-- #region [Describe] "Map"
-- #region [Child Describe] "constructors"
it("creates an empty array", function()
local foo = Map.new()
assert(foo.size == 0)
end)
it("creates a Map from an array", function()
local foo = Map.new({
{ AN_ITEM, "foo" },
{ ANOTHER_ITEM, "val" },
})
assert(foo.size == 2)
assert(foo:has(AN_ITEM) == true)
assert(foo:has(ANOTHER_ITEM) == true)
end)
it("creates a Map from an array with duplicate keys", function()
local foo = Map.new({
{ AN_ITEM, "foo1" },
{ AN_ITEM, "foo2" },
})
assert(foo.size == 1)
assert(foo:get(AN_ITEM) == "foo2")
assert(#foo:keys() == 1 and foo:keys()[1] == AN_ITEM)
assert(#foo:values() == 1 and foo:values()[1] == "foo2")
assert(#foo:entries() == 1)
assert(#foo:entries()[1] == 2)
assert(foo:entries()[1][1] == AN_ITEM)
assert(foo:entries()[1][2] == "foo2")
end)
it("preserves the order of keys first assignment", function()
local foo = Map.new({
{ AN_ITEM, "foo1" },
{ ANOTHER_ITEM, "bar" },
{ AN_ITEM, "foo2" },
})
assert(foo.size == 2)
assert(foo:get(AN_ITEM) == "foo2")
assert(foo:get(ANOTHER_ITEM) == "bar")
assert(foo:keys()[1] == AN_ITEM)
assert(foo:keys()[2] == ANOTHER_ITEM)
assert(foo:values()[1] == "foo2")
assert(foo:values()[2] == "bar")
assert(foo:entries()[1][1] == AN_ITEM)
assert(foo:entries()[1][2] == "foo2")
assert(foo:entries()[2][1] == ANOTHER_ITEM)
assert(foo:entries()[2][2] == "bar")
end)
-- #endregion
-- #region [Child Describe] "type"
it("instanceOf return true for an actual Map object", function()
local foo = Map.new()
assert(instanceOf(foo, Map) == true)
end)
it("instanceOf return false for an regular plain object", function()
local foo = {}
assert(instanceOf(foo, Map) == false)
end)
-- #endregion
-- #region [Child Describe] "set"
it("returns the Map object", function()
local foo = Map.new()
assert(foo:set(1, "baz") == foo)
end)
it("increments the size if the element is added for the first time", function()
local foo = Map.new()
foo:set(AN_ITEM, "foo")
assert(foo.size == 1)
end)
it("does not increment the size the second time an element is added", function()
local foo = Map.new()
foo:set(AN_ITEM, "foo")
foo:set(AN_ITEM, "val")
assert(foo.size == 1)
end)
it("sets values correctly to true/false", function()
-- Luau FIXME: Luau insists that arrays can't be mixed type
local foo = Map.new({ { AN_ITEM, false :: any } })
foo:set(AN_ITEM, false)
assert(foo.size == 1)
assert(foo:get(AN_ITEM) == false)
foo:set(AN_ITEM, true)
assert(foo.size == 1)
assert(foo:get(AN_ITEM) == true)
foo:set(AN_ITEM, false)
assert(foo.size == 1)
assert(foo:get(AN_ITEM) == false)
end)
-- #endregion
-- #region [Child Describe] "get"
it("returns value of item from provided key", function()
local foo = Map.new()
foo:set(AN_ITEM, "foo")
assert(foo:get(AN_ITEM) == "foo")
end)
it("returns nil if the item is not in the Map", function()
local foo = Map.new()
assert(foo:get(AN_ITEM) == nil)
end)
-- #endregion
-- #region [Child Describe] "clear"
it("sets the size to zero", function()
local foo = Map.new()
foo:set(AN_ITEM, "foo")
foo:clear()
assert(foo.size == 0)
end)
it("removes the items from the Map", function()
local foo = Map.new()
foo:set(AN_ITEM, "foo")
foo:clear()
assert(foo:has(AN_ITEM) == false)
end)
-- #endregion
-- #region [Child Describe] "delete"
it("removes the items from the Map", function()
local foo = Map.new()
foo:set(AN_ITEM, "foo")
foo:delete(AN_ITEM)
assert(foo:has(AN_ITEM) == false)
end)
it("returns true if the item was in the Map", function()
local foo = Map.new()
foo:set(AN_ITEM, "foo")
assert(foo:delete(AN_ITEM) == true)
end)
it("returns false if the item was not in the Map", function()
local foo = Map.new()
assert(foo:delete(AN_ITEM) == false)
end)
it("decrements the size if the item was in the Map", function()
local foo = Map.new()
foo:set(AN_ITEM, "foo")
foo:delete(AN_ITEM)
assert(foo.size == 0)
end)
it("does not decrement the size if the item was not in the Map", function()
local foo = Map.new()
foo:set(AN_ITEM, "foo")
foo:delete(ANOTHER_ITEM)
assert(foo.size == 1)
end)
it("deletes value set to false", function()
-- Luau FIXME: Luau insists arrays can't be mixed type
local foo = Map.new({ { AN_ITEM, false :: any } })
foo:delete(AN_ITEM)
assert(foo.size == 0)
assert(foo:get(AN_ITEM) == nil)
end)
-- #endregion
-- #region [Child Describe] "has"
it("returns true if the item is in the Map", function()
local foo = Map.new()
foo:set(AN_ITEM, "foo")
assert(foo:has(AN_ITEM) == true)
end)
it("returns false if the item is not in the Map", function()
local foo = Map.new()
assert(foo:has(AN_ITEM) == false)
end)
it("returns correctly with value set to false", function()
-- Luau FIXME: Luau insists arrays can't be mixed type
local foo = Map.new({ { AN_ITEM, false :: any } })
assert(foo:has(AN_ITEM) == true)
end)
-- #endregion
-- #region [Child Describe] "keys / values / entries"
it("returns array of elements", function()
local myMap = Map.new()
myMap:set(AN_ITEM, "foo")
myMap:set(ANOTHER_ITEM, "val")
assert(myMap:keys()[1] == AN_ITEM)
assert(myMap:keys()[2] == ANOTHER_ITEM)
assert(myMap:values()[1] == "foo")
assert(myMap:values()[2] == "val")
assert(myMap:entries()[1][1] == AN_ITEM)
assert(myMap:entries()[1][2] == "foo")
assert(myMap:entries()[2][1] == ANOTHER_ITEM)
assert(myMap:entries()[2][2] == "val")
end)
-- #endregion
-- #region [Child Describe] "__index"
it("can access fields directly without using get", function()
local typeName = "size"
local foo = Map.new({
{ AN_ITEM, "foo" },
{ ANOTHER_ITEM, "val" },
{ typeName, "buzz" },
})
assert(foo.size == 3)
assert(foo[AN_ITEM] == "foo")
assert(foo[ANOTHER_ITEM] == "val")
assert(foo:get(typeName) == "buzz")
end)
-- #endregion
-- #region [Child Describe] "__newindex"
it("can set fields directly without using set", function()
local foo = Map.new()
assert(foo.size == 0)
foo[AN_ITEM] = "foo"
foo[ANOTHER_ITEM] = "val"
foo.fizz = "buzz"
assert(foo.size == 3)
assert(foo:get(AN_ITEM) == "foo")
assert(foo:get(ANOTHER_ITEM) == "val")
assert(foo:get("fizz") == "buzz")
end)
-- #endregion
-- #region [Child Describe] "ipairs"
local function makeArray(...)
local array = {}
for _, item in ... do
table.insert(array, item)
end
return array
end
it("iterates on the elements by their insertion order", function()
local foo = Map.new()
foo:set(AN_ITEM, "foo")
foo:set(ANOTHER_ITEM, "val")
assert(makeArray(foo:ipairs())[1][1] == AN_ITEM)
assert(makeArray(foo:ipairs())[1][2] == "foo")
assert(makeArray(foo:ipairs())[2][1] == ANOTHER_ITEM)
assert(makeArray(foo:ipairs())[2][2] == "val")
end)
it("does not iterate on removed elements", function()
local foo = Map.new()
foo:set(AN_ITEM, "foo")
foo:set(ANOTHER_ITEM, "val")
foo:delete(AN_ITEM)
assert(makeArray(foo:ipairs())[1][1] == ANOTHER_ITEM)
assert(makeArray(foo:ipairs())[1][2] == "val")
end)
it("iterates on elements if the added back to the Map", function()
local foo = Map.new()
foo:set(AN_ITEM, "foo")
foo:set(ANOTHER_ITEM, "val")
foo:delete(AN_ITEM)
foo:set(AN_ITEM, "food")
assert(makeArray(foo:ipairs())[1][1] == ANOTHER_ITEM)
assert(makeArray(foo:ipairs())[1][2] == "val")
assert(makeArray(foo:ipairs())[2][1] == AN_ITEM)
assert(makeArray(foo:ipairs())[2][2] == "food")
end)
-- #endregion
-- #region [Child Describe] "Integration Tests"
-- it("MDN Examples", function()
-- local myMap = Map.new() :: Map<string | Object | Function, string>
-- local keyString = "a string"
-- local keyObj = {}
-- local keyFunc = function() end
-- -- setting the values
-- myMap:set(keyString, "value associated with 'a string'")
-- myMap:set(keyObj, "value associated with keyObj")
-- myMap:set(keyFunc, "value associated with keyFunc")
-- assert(myMap.size == 3)
-- -- getting the values
-- assert(myMap:get(keyString) == "value associated with 'a string'")
-- assert(myMap:get(keyObj) == "value associated with keyObj")
-- assert(myMap:get(keyFunc) == "value associated with keyFunc")
-- assert(myMap:get("a string") == "value associated with 'a string'")
-- assert(myMap:get({}) == nil) -- nil, because keyObj !== {}
-- assert(myMap:get(function() -- nil because keyFunc !== function () {}
-- end) == nil)
-- end)
it("handles non-traditional keys", function()
local myMap = Map.new() :: Map<boolean | number | string, string>
local falseKey = false
local trueKey = true
local negativeKey = -1
local emptyKey = ""
myMap:set(falseKey, "apple")
myMap:set(trueKey, "bear")
myMap:set(negativeKey, "corgi")
myMap:set(emptyKey, "doge")
assert(myMap.size == 4)
assert(myMap:get(falseKey) == "apple")
assert(myMap:get(trueKey) == "bear")
assert(myMap:get(negativeKey) == "corgi")
assert(myMap:get(emptyKey) == "doge")
myMap:delete(falseKey)
myMap:delete(trueKey)
myMap:delete(negativeKey)
myMap:delete(emptyKey)
assert(myMap.size == 0)
end)
-- #endregion
-- #endregion [Describe] "Map"
-- #region [Describe] "coerceToMap"
it("returns the same object if instance of Map", function()
local map = Map.new()
assert(coerceToMap(map) == map)
map = Map.new({})
assert(coerceToMap(map) == map)
map = Map.new({ { AN_ITEM, "foo" } })
assert(coerceToMap(map) == map)
end)
-- #endregion [Describe] "coerceToMap"
-- #endregion Tests to verify it works as expected

2089
bench/other/regex.lua Normal file

File diff suppressed because it is too large Load diff

View file

@ -213,6 +213,16 @@ TEST_CASE_FIXTURE(AssemblyBuilderX64Fixture, "FormsOfLea")
SINGLE_COMPARE(lea(rax, qword[r13 + r12 * 4 + 4]), 0x4b, 0x8d, 0x44, 0xa5, 0x04);
}
TEST_CASE_FIXTURE(AssemblyBuilderX64Fixture, "FormsOfAbsoluteJumps")
{
SINGLE_COMPARE(jmp(rax), 0x48, 0xff, 0xe0);
SINGLE_COMPARE(jmp(r14), 0x49, 0xff, 0xe6);
SINGLE_COMPARE(jmp(qword[r14 + rdx * 4]), 0x49, 0xff, 0x24, 0x96);
SINGLE_COMPARE(call(rax), 0x48, 0xff, 0xd0);
SINGLE_COMPARE(call(r14), 0x49, 0xff, 0xd6);
SINGLE_COMPARE(call(qword[r14 + rdx * 4]), 0x49, 0xff, 0x14, 0x96);
}
TEST_CASE_FIXTURE(AssemblyBuilderX64Fixture, "ControlFlow")
{
// Jump back
@ -260,6 +270,23 @@ TEST_CASE_FIXTURE(AssemblyBuilderX64Fixture, "ControlFlow")
{0xe9, 0x04, 0x00, 0x00, 0x00, 0x48, 0x83, 0xe7, 0x3e});
}
TEST_CASE_FIXTURE(AssemblyBuilderX64Fixture, "LabelCall")
{
check(
[](AssemblyBuilderX64& build) {
Label fnB;
build.and_(rcx, 0x3e);
build.call(fnB);
build.ret();
build.setLabel(fnB);
build.lea(rax, qword[rcx + 0x1f]);
build.ret();
},
{0x48, 0x83, 0xe1, 0x3e, 0xe8, 0x01, 0x00, 0x00, 0x00, 0xc3, 0x48, 0x8d, 0x41, 0x1f, 0xc3});
}
TEST_CASE_FIXTURE(AssemblyBuilderX64Fixture, "AVXBinaryInstructionForms")
{
SINGLE_COMPARE(vaddpd(xmm8, xmm10, xmm14), 0xc4, 0x41, 0xa9, 0x58, 0xc6);

View file

@ -105,4 +105,37 @@ if true then
REQUIRE(parentStat->is<AstStatIf>());
}
TEST_CASE_FIXTURE(Fixture, "ac_ast_ancestry_at_number_const")
{
check(R"(
print(3.)
)");
std::vector<AstNode*> ancestry = findAncestryAtPositionForAutocomplete(*getMainSourceModule(), Position(1, 8));
REQUIRE_GE(ancestry.size(), 2);
REQUIRE(ancestry.back()->is<AstExprConstantNumber>());
}
TEST_CASE_FIXTURE(Fixture, "ac_ast_ancestry_in_workspace_dot")
{
check(R"(
print(workspace.)
)");
std::vector<AstNode*> ancestry = findAncestryAtPositionForAutocomplete(*getMainSourceModule(), Position(1, 16));
REQUIRE_GE(ancestry.size(), 2);
REQUIRE(ancestry.back()->is<AstExprIndexName>());
}
TEST_CASE_FIXTURE(Fixture, "ac_ast_ancestry_in_workspace_colon")
{
check(R"(
print(workspace:)
)");
std::vector<AstNode*> ancestry = findAncestryAtPositionForAutocomplete(*getMainSourceModule(), Position(1, 16));
REQUIRE_GE(ancestry.size(), 2);
REQUIRE(ancestry.back()->is<AstExprIndexName>());
}
TEST_SUITE_END();

View file

@ -128,6 +128,7 @@ struct Fixture
std::optional<TypeId> lookupImportedType(const std::string& moduleAlias, const std::string& name);
ScopedFastFlag sff_DebugLuauFreezeArena;
ScopedFastFlag sff_UnknownNever{"LuauUnknownAndNeverType", true};
TestFileResolver fileResolver;
TestConfigResolver configResolver;

View file

@ -791,6 +791,8 @@ TEST_CASE_FIXTURE(FrontendFixture, "discard_type_graphs")
CHECK_EQ(0, module->internalTypes.typeVars.size());
CHECK_EQ(0, module->internalTypes.typePacks.size());
CHECK_EQ(0, module->astTypes.size());
CHECK_EQ(0, module->astResolvedTypes.size());
CHECK_EQ(0, module->astResolvedTypePacks.size());
}
TEST_CASE_FIXTURE(FrontendFixture, "it_should_be_safe_to_stringify_errors_when_full_type_graph_is_discarded")

View file

@ -301,8 +301,6 @@ TEST_CASE_FIXTURE(Fixture, "clone_recursion_limit")
TEST_CASE_FIXTURE(Fixture, "any_persistance_does_not_leak")
{
ScopedFastFlag luauNonCopyableTypeVarFields{"LuauNonCopyableTypeVarFields", true};
fileResolver.source["Module/A"] = R"(
export type A = B
type B = A

View file

@ -1055,8 +1055,6 @@ export type t1 = { a: typeof(string.byte) }
TEST_CASE_FIXTURE(Fixture, "intersection_combine_on_bound_self")
{
ScopedFastFlag luauNormalizeCombineEqFix{"LuauNormalizeCombineEqFix", true};
CheckResult result = check(R"(
export type t0 = (((any)&({_:l0.t0,n0:t0,_G:any,}))&({_:any,}))&(((any)&({_:l0.t0,n0:t0,_G:any,}))&({_:any,}))
)");
@ -1064,6 +1062,46 @@ export type t0 = (((any)&({_:l0.t0,n0:t0,_G:any,}))&({_:any,}))&(((any)&({_:l0.t
LUAU_REQUIRE_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "normalize_unions_containing_never")
{
ScopedFastFlag sff{"LuauLowerBoundsCalculation", true};
CheckResult result = check(R"(
type Foo = string | never
local foo: Foo
)");
CHECK_EQ("string", toString(requireType("foo")));
}
TEST_CASE_FIXTURE(Fixture, "normalize_unions_containing_unknown")
{
ScopedFastFlag sff{"LuauLowerBoundsCalculation", true};
CheckResult result = check(R"(
type Foo = string | unknown
local foo: Foo
)");
CHECK_EQ("unknown", toString(requireType("foo")));
}
TEST_CASE_FIXTURE(Fixture, "any_wins_the_battle_over_unknown_in_unions")
{
ScopedFastFlag sff{"LuauLowerBoundsCalculation", true};
CheckResult result = check(R"(
type Foo = unknown | any
local foo: Foo
type Bar = any | unknown
local bar: Bar
)");
CHECK_EQ("any", toString(requireType("foo")));
CHECK_EQ("any", toString(requireType("bar")));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "normalization_does_not_convert_ever")
{
ScopedFastFlag sff[]{

View file

@ -2648,7 +2648,6 @@ type Z<T> = { a: string | T..., b: number }
TEST_CASE_FIXTURE(Fixture, "recover_function_return_type_annotations")
{
ScopedFastFlag sff{"LuauReturnTypeTokenConfusion", true};
ParseResult result = tryParse(R"(
type Custom<A, B, C> = { x: A, y: B, z: C }
type Packed<A...> = { x: (A...) -> () }

View file

@ -96,6 +96,37 @@ TEST_CASE_FIXTURE(Fixture, "table_respects_use_line_break")
//clang-format on
}
TEST_CASE_FIXTURE(Fixture, "metatable")
{
TypeVar table{TypeVariant(TableTypeVar())};
TypeVar metatable{TypeVariant(TableTypeVar())};
TypeVar mtv{TypeVariant(MetatableTypeVar{&table, &metatable})};
CHECK_EQ("{ @metatable { }, { } }", toString(&mtv));
}
TEST_CASE_FIXTURE(Fixture, "named_metatable")
{
TypeVar table{TypeVariant(TableTypeVar())};
TypeVar metatable{TypeVariant(TableTypeVar())};
TypeVar mtv{TypeVariant(MetatableTypeVar{&table, &metatable, "NamedMetatable"})};
CHECK_EQ("NamedMetatable", toString(&mtv));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "named_metatable_toStringNamedFunction")
{
CheckResult result = check(R"(
local function createTbl(): NamedMetatable
return setmetatable({}, {})
end
type NamedMetatable = typeof(createTbl())
)");
TypeId ty = requireType("createTbl");
const FunctionTypeVar* ftv = get<FunctionTypeVar>(follow(ty));
REQUIRE(ftv);
CHECK_EQ("createTbl(): NamedMetatable", toStringNamedFunction("createTbl", *ftv));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "exhaustive_toString_of_cyclic_table")
{
CheckResult result = check(R"(
@ -468,7 +499,7 @@ local function target(callback: nil) return callback(4, "hello") end
)");
LUAU_REQUIRE_ERRORS(result);
CHECK_EQ("(nil) -> (*unknown*)", toString(requireType("target")));
CHECK_EQ("(nil) -> (<error-type>)", toString(requireType("target")));
}
TEST_CASE_FIXTURE(Fixture, "toStringGenericPack")

View file

@ -583,7 +583,7 @@ TEST_CASE_FIXTURE(Fixture, "transpile_error_expr")
auto names = AstNameTable{allocator};
ParseResult parseResult = Parser::parse(code.data(), code.size(), names, allocator, {});
CHECK_EQ("local a = (error-expr: f.%error-id%)-(error-expr)", transpileWithTypes(*parseResult.root));
CHECK_EQ("local a = (error-expr: f:%error-id%)-(error-expr)", transpileWithTypes(*parseResult.root));
}
TEST_CASE_FIXTURE(Fixture, "transpile_error_stat")

View file

@ -94,7 +94,7 @@ TEST_CASE_FIXTURE(Fixture, "for_in_loop_iterator_is_error")
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ("*unknown*", toString(requireType("a")));
CHECK_EQ("<error-type>", toString(requireType("a")));
}
TEST_CASE_FIXTURE(Fixture, "for_in_loop_iterator_is_error2")
@ -110,7 +110,7 @@ TEST_CASE_FIXTURE(Fixture, "for_in_loop_iterator_is_error2")
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ("*unknown*", toString(requireType("a")));
CHECK_EQ("<error-type>", toString(requireType("a")));
}
TEST_CASE_FIXTURE(Fixture, "length_of_error_type_does_not_produce_an_error")
@ -225,7 +225,7 @@ TEST_CASE_FIXTURE(Fixture, "calling_error_type_yields_error")
CHECK_EQ("unknown", err->name);
CHECK_EQ("*unknown*", toString(requireType("a")));
CHECK_EQ("<error-type>", toString(requireType("a")));
}
TEST_CASE_FIXTURE(Fixture, "chain_calling_error_type_yields_error")
@ -234,7 +234,7 @@ TEST_CASE_FIXTURE(Fixture, "chain_calling_error_type_yields_error")
local a = Utility.Create "Foo" {}
)");
CHECK_EQ("*unknown*", toString(requireType("a")));
CHECK_EQ("<error-type>", toString(requireType("a")));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "replace_every_free_type_when_unifying_a_complex_function_with_any")

View file

@ -925,7 +925,7 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "assert_returns_false_and_string_iff_it_knows
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("(nil) -> nil", toString(requireType("f")));
CHECK_EQ("(nil) -> (never, ...never)", toString(requireType("f")));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "table_freeze_is_generic")
@ -952,7 +952,7 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "table_freeze_is_generic")
CHECK_EQ("number", toString(requireType("a")));
CHECK_EQ("string", toString(requireType("b")));
CHECK_EQ("boolean", toString(requireType("c")));
CHECK_EQ("*unknown*", toString(requireType("d")));
CHECK_EQ("<error-type>", toString(requireType("d")));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "set_metatable_needs_arguments")
@ -965,8 +965,8 @@ a:b()
a:b({})
)");
LUAU_REQUIRE_ERROR_COUNT(2, result);
CHECK_EQ(result.errors[0], (TypeError{Location{{2, 0}, {2, 5}}, CountMismatch{2, 0}}));
CHECK_EQ(result.errors[1], (TypeError{Location{{3, 0}, {3, 5}}, CountMismatch{2, 1}}));
CHECK_EQ(toString(result.errors[0]), "Argument count mismatch. Function expects 2 arguments, but none are specified");
CHECK_EQ(toString(result.errors[1]), "Argument count mismatch. Function expects 2 arguments, but only 1 is specified");
}
TEST_CASE_FIXTURE(Fixture, "typeof_unresolved_function")
@ -1008,4 +1008,139 @@ end
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(BuiltinsFixture, "gmatch_capture_types")
{
ScopedFastFlag sffs{"LuauDeduceGmatchReturnTypes", true};
CheckResult result = check(R"END(
local a, b, c = string.gmatch("This is a string", "(.()(%a+))")()
)END");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ(toString(requireType("a")), "string");
CHECK_EQ(toString(requireType("b")), "number");
CHECK_EQ(toString(requireType("c")), "string");
}
TEST_CASE_FIXTURE(BuiltinsFixture, "gmatch_capture_types2")
{
ScopedFastFlag sffs{"LuauDeduceGmatchReturnTypes", true};
CheckResult result = check(R"END(
local a, b, c = ("This is a string"):gmatch("(.()(%a+))")()
)END");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ(toString(requireType("a")), "string");
CHECK_EQ(toString(requireType("b")), "number");
CHECK_EQ(toString(requireType("c")), "string");
}
TEST_CASE_FIXTURE(BuiltinsFixture, "gmatch_capture_types_default_capture")
{
ScopedFastFlag sffs{"LuauDeduceGmatchReturnTypes", true};
CheckResult result = check(R"END(
local a, b, c, d = string.gmatch("T(his)() is a string", ".")()
)END");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CountMismatch* acm = get<CountMismatch>(result.errors[0]);
REQUIRE(acm);
CHECK_EQ(acm->context, CountMismatch::Result);
CHECK_EQ(acm->expected, 1);
CHECK_EQ(acm->actual, 4);
CHECK_EQ(toString(requireType("a")), "string");
}
TEST_CASE_FIXTURE(BuiltinsFixture, "gmatch_capture_types_balanced_escaped_parens")
{
ScopedFastFlag sffs{"LuauDeduceGmatchReturnTypes", true};
CheckResult result = check(R"END(
local a, b, c, d = string.gmatch("T(his) is a string", "((.)%b()())")()
)END");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CountMismatch* acm = get<CountMismatch>(result.errors[0]);
REQUIRE(acm);
CHECK_EQ(acm->context, CountMismatch::Result);
CHECK_EQ(acm->expected, 3);
CHECK_EQ(acm->actual, 4);
CHECK_EQ(toString(requireType("a")), "string");
CHECK_EQ(toString(requireType("b")), "string");
CHECK_EQ(toString(requireType("c")), "number");
}
TEST_CASE_FIXTURE(BuiltinsFixture, "gmatch_capture_types_parens_in_sets_are_ignored")
{
ScopedFastFlag sffs{"LuauDeduceGmatchReturnTypes", true};
CheckResult result = check(R"END(
local a, b, c = string.gmatch("T(his)() is a string", "(T[()])()")()
)END");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CountMismatch* acm = get<CountMismatch>(result.errors[0]);
REQUIRE(acm);
CHECK_EQ(acm->context, CountMismatch::Result);
CHECK_EQ(acm->expected, 2);
CHECK_EQ(acm->actual, 3);
CHECK_EQ(toString(requireType("a")), "string");
CHECK_EQ(toString(requireType("b")), "number");
}
TEST_CASE_FIXTURE(BuiltinsFixture, "gmatch_capture_types_set_containing_lbracket")
{
ScopedFastFlag sffs{"LuauDeduceGmatchReturnTypes", true};
CheckResult result = check(R"END(
local a, b = string.gmatch("[[[", "()([[])")()
)END");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ(toString(requireType("a")), "number");
CHECK_EQ(toString(requireType("b")), "string");
}
TEST_CASE_FIXTURE(BuiltinsFixture, "gmatch_capture_types_leading_end_bracket_is_part_of_set")
{
CheckResult result = check(R"END(
-- An immediate right-bracket following a left-bracket is included within the set;
-- thus, '[]]'' is the set containing ']', and '[]' is an invalid set missing an enclosing
-- right-bracket. We detect an invalid set in this case and fall back to to default gmatch
-- typing.
local foo = string.gmatch("T[hi%]s]]]() is a string", "([]s)")
)END");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ(toString(requireType("foo")), "() -> (...string)");
}
TEST_CASE_FIXTURE(BuiltinsFixture, "gmatch_capture_types_invalid_pattern_fallback_to_builtin")
{
CheckResult result = check(R"END(
local foo = string.gmatch("T(his)() is a string", ")")
)END");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ(toString(requireType("foo")), "() -> (...string)");
}
TEST_CASE_FIXTURE(BuiltinsFixture, "gmatch_capture_types_invalid_pattern_fallback_to_builtin2")
{
CheckResult result = check(R"END(
local foo = string.gmatch("T(his)() is a string", "[")
)END");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ(toString(requireType("foo")), "() -> (...string)");
}
TEST_SUITE_END();

View file

@ -916,13 +916,13 @@ TEST_CASE_FIXTURE(Fixture, "function_cast_error_uses_correct_language")
REQUIRE(tm1);
CHECK_EQ("(string) -> number", toString(tm1->wantedType));
CHECK_EQ("(string, *unknown*) -> number", toString(tm1->givenType));
CHECK_EQ("(string, <error-type>) -> number", toString(tm1->givenType));
auto tm2 = get<TypeMismatch>(result.errors[1]);
REQUIRE(tm2);
CHECK_EQ("(number, number) -> (number, number)", toString(tm2->wantedType));
CHECK_EQ("(string, *unknown*) -> number", toString(tm2->givenType));
CHECK_EQ("(string, <error-type>) -> number", toString(tm2->givenType));
}
TEST_CASE_FIXTURE(Fixture, "no_lossy_function_type")
@ -1535,7 +1535,7 @@ function t:b() return 2 end -- not OK
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(R"(Type '(*unknown*) -> number' could not be converted into '() -> number'
CHECK_EQ(R"(Type '(<error-type>) -> number' could not be converted into '() -> number'
caused by:
Argument count mismatch. Function expects 1 argument, but none are specified)",
toString(result.errors[0]));
@ -1692,4 +1692,52 @@ TEST_CASE_FIXTURE(Fixture, "call_o_with_another_argument_after_foo_was_quantifie
// TODO: check the normalized type of f
}
TEST_CASE_FIXTURE(Fixture, "free_is_not_bound_to_unknown")
{
CheckResult result = check(R"(
local function foo(f: (unknown) -> (), x)
f(x)
end
)");
CHECK_EQ("<a>((unknown) -> (), a) -> ()", toString(requireType("foo")));
}
TEST_CASE_FIXTURE(Fixture, "dont_infer_parameter_types_for_functions_from_their_call_site")
{
CheckResult result = check(R"(
local t = {}
function t.f(x)
return x
end
t.__index = t
function g(s)
local q = s.p and s.p.q or nil
return q and t.f(q) or nil
end
local f = t.f
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("<a>(a) -> a", toString(requireType("f")));
}
TEST_CASE_FIXTURE(Fixture, "dont_mutate_the_underlying_head_of_typepack_when_calling_with_self")
{
CheckResult result = check(R"(
local t = {}
function t:m(x) end
function f(): never return 5 :: never end
t:m(f())
t:m(f())
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_SUITE_END();

View file

@ -9,6 +9,8 @@
#include "doctest.h"
LUAU_FASTFLAG(LuauCheckGenericHOFTypes)
using namespace Luau;
TEST_SUITE_BEGIN("GenericsTests");
@ -1001,7 +1003,7 @@ TEST_CASE_FIXTURE(Fixture, "no_stack_overflow_from_quantifying")
std::optional<TypeFun> t0 = getMainModule()->getModuleScope()->lookupType("t0");
REQUIRE(t0);
CHECK_EQ("*unknown*", toString(t0->type));
CHECK_EQ("<error-type>", toString(t0->type));
auto it = std::find_if(result.errors.begin(), result.errors.end(), [](TypeError& err) {
return get<OccursCheckFailed>(err);
@ -1095,10 +1097,18 @@ local b = sumrec(sum) -- ok
local c = sumrec(function(x, y, f) return f(x, y) end) -- type binders are not inferred
)");
LUAU_REQUIRE_ERRORS(result);
CHECK_EQ("Type '(a, b, (a, b) -> (c...)) -> (c...)' could not be converted into '<a>(a, a, (a, a) -> a) -> a'; different number of generic type "
"parameters",
toString(result.errors[0]));
if (FFlag::LuauCheckGenericHOFTypes)
{
LUAU_REQUIRE_NO_ERRORS(result);
}
else
{
LUAU_REQUIRE_ERRORS(result);
CHECK_EQ(
"Type '(a, b, (a, b) -> (c...)) -> (c...)' could not be converted into '<a>(a, a, (a, a) -> a) -> a'; different number of generic type "
"parameters",
toString(result.errors[0]));
}
}
TEST_CASE_FIXTURE(Fixture, "substitution_with_bound_table")
@ -1185,4 +1195,23 @@ TEST_CASE_FIXTURE(Fixture, "quantify_functions_even_if_they_have_an_explicit_gen
CHECK("<X, a...>((X) -> (a...), X) -> (a...)" == toString(requireType("foo")));
}
TEST_CASE_FIXTURE(Fixture, "do_not_always_instantiate_generic_intersection_types")
{
ScopedFastFlag sff[] = {
{"LuauMaybeGenericIntersectionTypes", true},
};
CheckResult result = check(R"(
--!strict
type Array<T> = { [number]: T }
type Array_Statics = {
new: <T>() -> Array<T>,
}
local _Arr : Array<any> & Array_Statics = {} :: Array_Statics
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_SUITE_END();

View file

@ -142,7 +142,7 @@ TEST_CASE_FIXTURE(Fixture, "for_in_loop_on_error")
CHECK_EQ(2, result.errors.size());
TypeId p = requireType("p");
CHECK_EQ("*unknown*", toString(p));
CHECK_EQ("<error-type>", toString(p));
}
TEST_CASE_FIXTURE(Fixture, "for_in_loop_on_non_function")

View file

@ -143,7 +143,7 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "require_module_that_does_not_export")
auto hootyType = requireType(bModule, "Hooty");
CHECK_EQ("*unknown*", toString(hootyType));
CHECK_EQ("<error-type>", toString(hootyType));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "warn_if_you_try_to_require_a_non_modulescript")
@ -244,7 +244,7 @@ local ModuleA = require(game.A)
LUAU_REQUIRE_NO_ERRORS(result);
std::optional<TypeId> oty = requireType("ModuleA");
CHECK_EQ("*unknown*", toString(*oty));
CHECK_EQ("<error-type>", toString(*oty));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "do_not_modify_imported_types")
@ -302,6 +302,30 @@ type Rename = typeof(x.x)
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(BuiltinsFixture, "do_not_modify_imported_types_4")
{
fileResolver.source["game/A"] = R"(
export type Array<T> = {T}
local arrayops = {}
function arrayops.foo(x: Array<any>) end
return arrayops
)";
CheckResult result = check(R"(
local arrayops = require(game.A)
local tbl = {}
tbl.a = 2
function tbl:foo(b: number, c: number)
-- introduce BoundTypeVar to imported type
arrayops.foo(self._regions)
end
type Table = typeof(tbl)
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(BuiltinsFixture, "module_type_conflict")
{
fileResolver.source["game/A"] = R"(
@ -363,4 +387,21 @@ caused by:
Property 'x' is not compatible. Type 'number' could not be converted into 'string')");
}
TEST_CASE_FIXTURE(BuiltinsFixture, "constrained_anyification_clone_immutable_types")
{
ScopedFastFlag luauAnyificationMustClone{"LuauAnyificationMustClone", true};
fileResolver.source["game/A"] = R"(
return function(...) end
)";
fileResolver.source["game/B"] = R"(
local l0 = require(game.A)
return l0
)";
CheckResult result = frontend.check("game/B");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_SUITE_END();

View file

@ -871,4 +871,26 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "equality_operations_succeed_if_any_union_bra
CHECK(toString(result2.errors[0]) == "Types Foo and Bar cannot be compared with == because they do not have the same metatable");
}
TEST_CASE_FIXTURE(BuiltinsFixture, "expected_types_through_binary_and")
{
ScopedFastFlag sff{"LuauBinaryNeedsExpectedTypesToo", true};
CheckResult result = check(R"(
local x: "a" | "b" | boolean = math.random() > 0.5 and "a"
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(BuiltinsFixture, "expected_types_through_binary_or")
{
ScopedFastFlag sff{"LuauBinaryNeedsExpectedTypesToo", true};
CheckResult result = check(R"(
local x: "a" | "b" | boolean = math.random() > 0.5 or "b"
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_SUITE_END();

View file

@ -47,7 +47,7 @@ TEST_CASE_FIXTURE(Fixture, "string_index")
REQUIRE(nat);
CHECK_EQ("string", toString(nat->ty));
CHECK_EQ("*unknown*", toString(requireType("t")));
CHECK_EQ("<error-type>", toString(requireType("t")));
}
TEST_CASE_FIXTURE(Fixture, "string_method")

View file

@ -225,7 +225,7 @@ TEST_CASE_FIXTURE(Fixture, "discriminate_from_x_not_equal_to_nil")
CHECK_EQ("{| x: nil, y: nil |} | {| x: string, y: number |}", toString(requireTypeAtPosition({7, 28})));
}
TEST_CASE_FIXTURE(Fixture, "bail_early_if_unification_is_too_complicated" * doctest::timeout(0.5))
TEST_CASE_FIXTURE(BuiltinsFixture, "bail_early_if_unification_is_too_complicated" * doctest::timeout(0.5))
{
ScopedFastInt sffi{"LuauTarjanChildLimit", 1};
ScopedFastInt sffi2{"LuauTypeInferIterationLimit", 1};
@ -499,6 +499,17 @@ TEST_CASE_FIXTURE(Fixture, "constrained_is_level_dependent")
CHECK_EQ("<a...>(t1) -> {| [t1]: boolean |} where t1 = t2 ; t2 = {+ m1: (t1) -> (a...), m2: (t2) -> (b...) +}", toString(requireType("f")));
}
TEST_CASE_FIXTURE(Fixture, "free_is_not_bound_to_any")
{
CheckResult result = check(R"(
local function foo(f: (any) -> (), x)
f(x)
end
)");
CHECK_EQ("((any) -> (), any) -> ()", toString(requireType("foo")));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "greedy_inference_with_shared_self_triggers_function_with_no_returns")
{
ScopedFastFlag sff{"DebugLuauSharedSelf", true};
@ -518,7 +529,7 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "greedy_inference_with_shared_self_triggers_f
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ("Not all codepaths in this function return '{ @metatable T, {| |} }, a...'.", toString(result.errors[0]));
CHECK_EQ("Not all codepaths in this function return 'self, a...'.", toString(result.errors[0]));
}
TEST_SUITE_END();

View file

@ -272,8 +272,8 @@ TEST_CASE_FIXTURE(Fixture, "typeguard_only_look_up_types_from_global_scope")
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("*unknown*", toString(requireTypeAtPosition({8, 44})));
CHECK_EQ("*unknown*", toString(requireTypeAtPosition({9, 38})));
CHECK_EQ("never", toString(requireTypeAtPosition({8, 44})));
CHECK_EQ("never", toString(requireTypeAtPosition({9, 38})));
}
TEST_CASE_FIXTURE(Fixture, "call_a_more_specific_function_using_typeguard")
@ -526,7 +526,7 @@ TEST_CASE_FIXTURE(Fixture, "type_narrow_to_vector")
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("*unknown*", toString(requireTypeAtPosition({3, 28})));
CHECK_EQ("<error-type>", toString(requireTypeAtPosition({3, 28})));
}
TEST_CASE_FIXTURE(Fixture, "nonoptional_type_can_narrow_to_nil_if_sense_is_true")
@ -651,7 +651,7 @@ TEST_CASE_FIXTURE(Fixture, "type_guard_can_filter_for_overloaded_function")
CHECK_EQ("nil", toString(requireTypeAtPosition({6, 28})));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "type_guard_warns_on_no_overlapping_types_only_when_sense_is_true")
TEST_CASE_FIXTURE(BuiltinsFixture, "type_guard_narrowed_into_nothingness")
{
CheckResult result = check(R"(
local function f(t: {x: number})
@ -666,7 +666,7 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "type_guard_warns_on_no_overlapping_types_onl
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("*unknown*", toString(requireTypeAtPosition({3, 28})));
CHECK_EQ("never", toString(requireTypeAtPosition({3, 28})));
}
TEST_CASE_FIXTURE(Fixture, "not_a_or_not_b")
@ -1074,7 +1074,7 @@ TEST_CASE_FIXTURE(RefinementClassFixture, "typeguard_cast_free_table_to_vector")
CHECK_EQ("Vector3", toString(requireTypeAtPosition({5, 28}))); // type(vec) == "vector"
CHECK_EQ("*unknown*", toString(requireTypeAtPosition({7, 28}))); // typeof(vec) == "Instance"
CHECK_EQ("never", toString(requireTypeAtPosition({7, 28}))); // typeof(vec) == "Instance"
CHECK_EQ("{+ X: a, Y: b, Z: c +}", toString(requireTypeAtPosition({9, 28}))); // type(vec) ~= "vector" and typeof(vec) ~= "Instance"
}
@ -1206,6 +1206,24 @@ TEST_CASE_FIXTURE(Fixture, "typeguard_doesnt_leak_to_elseif")
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(BuiltinsFixture, "refine_unknowns")
{
CheckResult result = check(R"(
local function f(x: unknown)
if type(x) == "string" then
local foo = x
else
local bar = x
end
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("string", toString(requireTypeAtPosition({3, 28})));
CHECK_EQ("unknown", toString(requireTypeAtPosition({5, 28})));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "falsiness_of_TruthyPredicate_narrows_into_nil")
{
ScopedFastFlag sff{"LuauFalsyPredicateReturnsNilInstead", true};
@ -1227,4 +1245,19 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "falsiness_of_TruthyPredicate_narrows_into_ni
CHECK_EQ("number", toString(requireTypeAtPosition({6, 28})));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "what_nonsensical_condition")
{
CheckResult result = check(R"(
local function f(x)
if type(x) == "string" and type(x) == "number" then
local foo = x
end
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("never", toString(requireTypeAtPosition({3, 28})));
}
TEST_SUITE_END();

View file

@ -3070,4 +3070,18 @@ TEST_CASE_FIXTURE(Fixture, "quantify_even_that_table_was_never_exported_at_all")
CHECK_EQ("{| m: <a, b>({+ x: a, y: b +}) -> a, n: <a, b>({+ x: a, y: b +}) -> b |}", toString(requireType("T"), opts));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "leaking_bad_metatable_errors")
{
ScopedFastFlag luauIndexSilenceErrors{"LuauIndexSilenceErrors", true};
CheckResult result = check(R"(
local a = setmetatable({}, 1)
local b = a.x
)");
LUAU_REQUIRE_ERROR_COUNT(2, result);
CHECK_EQ("Metatable was not a table", toString(result.errors[0]));
CHECK_EQ("Type 'a' does not have key 'x'", toString(result.errors[1]));
}
TEST_SUITE_END();

View file

@ -238,10 +238,10 @@ TEST_CASE_FIXTURE(Fixture, "type_errors_infer_types")
// TODO: Should we assert anything about these tests when DCR is being used?
if (!FFlag::DebugLuauDeferredConstraintResolution)
{
CHECK_EQ("*unknown*", toString(requireType("c")));
CHECK_EQ("*unknown*", toString(requireType("d")));
CHECK_EQ("*unknown*", toString(requireType("e")));
CHECK_EQ("*unknown*", toString(requireType("f")));
CHECK_EQ("<error-type>", toString(requireType("c")));
CHECK_EQ("<error-type>", toString(requireType("d")));
CHECK_EQ("<error-type>", toString(requireType("e")));
CHECK_EQ("<error-type>", toString(requireType("f")));
}
}
@ -622,7 +622,7 @@ TEST_CASE_FIXTURE(Fixture, "no_stack_overflow_from_isoptional")
std::optional<TypeFun> t0 = getMainModule()->getModuleScope()->lookupType("t0");
REQUIRE(t0);
CHECK_EQ("*unknown*", toString(t0->type));
CHECK_EQ("<error-type>", toString(t0->type));
auto it = std::find_if(result.errors.begin(), result.errors.end(), [](TypeError& err) {
return get<OccursCheckFailed>(err);
@ -1003,4 +1003,27 @@ TEST_CASE_FIXTURE(Fixture, "do_not_bind_a_free_table_to_a_union_containing_that_
)");
}
TEST_CASE_FIXTURE(Fixture, "types stored in astResolvedTypes")
{
CheckResult result = check(R"(
type alias = typeof("hello")
local function foo(param: alias)
end
)");
auto node = findNodeAtPosition(*getMainSourceModule(), {2, 16});
auto ty = lookupType("alias");
REQUIRE(node);
REQUIRE(node->is<AstExprFunction>());
REQUIRE(ty);
auto func = node->as<AstExprFunction>();
REQUIRE(func->args.size == 1);
auto arg = *func->args.begin();
auto annotation = arg->annotation;
CHECK_EQ(*getMainModule()->astResolvedTypes.find(annotation), *ty);
}
TEST_SUITE_END();

View file

@ -121,7 +121,7 @@ TEST_CASE_FIXTURE(TryUnifyFixture, "members_of_failed_typepack_unification_are_u
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ("a", toString(requireType("a")));
CHECK_EQ("*unknown*", toString(requireType("b")));
CHECK_EQ("<error-type>", toString(requireType("b")));
}
TEST_CASE_FIXTURE(TryUnifyFixture, "result_of_failed_typepack_unification_is_constrained")
@ -136,7 +136,7 @@ TEST_CASE_FIXTURE(TryUnifyFixture, "result_of_failed_typepack_unification_is_con
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ("a", toString(requireType("a")));
CHECK_EQ("*unknown*", toString(requireType("b")));
CHECK_EQ("<error-type>", toString(requireType("b")));
CHECK_EQ("number", toString(requireType("c")));
}

View file

@ -199,7 +199,7 @@ TEST_CASE_FIXTURE(Fixture, "index_on_a_union_type_with_missing_property")
CHECK_EQ(mup->missing[0], *bTy);
CHECK_EQ(mup->key, "x");
CHECK_EQ("*unknown*", toString(requireType("r")));
CHECK_EQ("<error-type>", toString(requireType("r")));
}
TEST_CASE_FIXTURE(Fixture, "index_on_a_union_type_with_one_property_of_type_any")

View file

@ -0,0 +1,280 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#include "Fixture.h"
#include "doctest.h"
using namespace Luau;
TEST_SUITE_BEGIN("TypeInferUnknownNever");
TEST_CASE_FIXTURE(Fixture, "string_subtype_and_unknown_supertype")
{
CheckResult result = check(R"(
local function f(x: string)
local foo: unknown = x
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "unknown_subtype_and_string_supertype")
{
CheckResult result = check(R"(
local function f(x: unknown)
local foo: string = x
end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
}
TEST_CASE_FIXTURE(Fixture, "unknown_is_reflexive")
{
CheckResult result = check(R"(
local function f(x: unknown)
local foo: unknown = x
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "string_subtype_and_never_supertype")
{
CheckResult result = check(R"(
local function f(x: string)
local foo: never = x
end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
}
TEST_CASE_FIXTURE(Fixture, "never_subtype_and_string_supertype")
{
CheckResult result = check(R"(
local function f(x: never)
local foo: string = x
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "never_is_reflexive")
{
CheckResult result = check(R"(
local function f(x: never)
local foo: never = x
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "unknown_is_optional_because_it_too_encompasses_nil")
{
CheckResult result = check(R"(
local t: {x: unknown} = {}
)");
}
TEST_CASE_FIXTURE(Fixture, "table_with_prop_of_type_never_is_uninhabitable")
{
CheckResult result = check(R"(
local t: {x: never} = {}
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
}
TEST_CASE_FIXTURE(Fixture, "table_with_prop_of_type_never_is_also_reflexive")
{
CheckResult result = check(R"(
local t: {x: never} = {x = 5 :: never}
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "array_like_table_of_never_is_inhabitable")
{
CheckResult result = check(R"(
local t: {never} = {}
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "type_packs_containing_never_is_itself_uninhabitable")
{
CheckResult result = check(R"(
local function f() return "foo", 5 :: never end
local x, y, z = f()
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("never", toString(requireType("x")));
CHECK_EQ("never", toString(requireType("y")));
CHECK_EQ("never", toString(requireType("z")));
}
TEST_CASE_FIXTURE(Fixture, "type_packs_containing_never_is_itself_uninhabitable2")
{
CheckResult result = check(R"(
local function f(): (string, never) return "", 5 :: never end
local function g(): (never, string) return 5 :: never, "" end
local x1, x2 = f()
local y1, y2 = g()
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("never", toString(requireType("x1")));
CHECK_EQ("never", toString(requireType("x2")));
CHECK_EQ("never", toString(requireType("y1")));
CHECK_EQ("never", toString(requireType("y2")));
}
TEST_CASE_FIXTURE(Fixture, "index_on_never")
{
CheckResult result = check(R"(
local x: never = 5 :: never
local z = x.y
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("never", toString(requireType("z")));
}
TEST_CASE_FIXTURE(Fixture, "call_never")
{
CheckResult result = check(R"(
local f: never = 5 :: never
local x, y, z = f()
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("never", toString(requireType("x")));
CHECK_EQ("never", toString(requireType("y")));
CHECK_EQ("never", toString(requireType("z")));
}
TEST_CASE_FIXTURE(Fixture, "assign_to_local_which_is_never")
{
CheckResult result = check(R"(
local t: never
t = 3
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "assign_to_global_which_is_never")
{
CheckResult result = check(R"(
--!nonstrict
t = 5 :: never
t = ""
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "assign_to_prop_which_is_never")
{
CheckResult result = check(R"(
local t: never
t.x = 5
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "assign_to_subscript_which_is_never")
{
CheckResult result = check(R"(
local t: never
t[5] = 7
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "assign_to_subscript_which_is_never")
{
CheckResult result = check(R"(
for i, v in (5 :: never) do
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "pick_never_from_variadic_type_pack")
{
CheckResult result = check(R"(
local function f(...: never)
local x, y = (...)
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "index_on_union_of_tables_for_properties_that_is_never")
{
CheckResult result = check(R"(
type Disjoint = {foo: never, bar: unknown, tag: "ok"} | {foo: never, baz: unknown, tag: "err"}
local disjoint: Disjoint = {foo = 5 :: never, bar = true, tag = "ok"}
local foo = disjoint.foo
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("never", toString(requireType("foo")));
}
TEST_CASE_FIXTURE(Fixture, "index_on_union_of_tables_for_properties_that_is_sorta_never")
{
CheckResult result = check(R"(
type Disjoint = {foo: string, bar: unknown, tag: "ok"} | {foo: never, baz: unknown, tag: "err"}
local disjoint: Disjoint = {foo = 5 :: never, bar = true, tag = "ok"}
local foo = disjoint.foo
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("string", toString(requireType("foo")));
}
TEST_CASE_FIXTURE(Fixture, "unary_minus_of_never")
{
CheckResult result = check(R"(
local x = -(5 :: never)
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("never", toString(requireType("x")));
}
TEST_CASE_FIXTURE(Fixture, "length_of_never")
{
CheckResult result = check(R"(
local x = #({} :: never)
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("never", toString(requireType("x")));
}
TEST_SUITE_END();

View file

@ -199,8 +199,6 @@ TEST_CASE_FIXTURE(TypePackFixture, "std_distance")
TEST_CASE("content_reassignment")
{
ScopedFastFlag luauNonCopyableTypeVarFields{"LuauNonCopyableTypeVarFields", true};
TypePackVar myError{Unifiable::Error{}, /*presistent*/ true};
TypeArena arena;

View file

@ -418,8 +418,6 @@ TEST_CASE("proof_that_isBoolean_uses_all_of")
TEST_CASE("content_reassignment")
{
ScopedFastFlag luauNonCopyableTypeVarFields{"LuauNonCopyableTypeVarFields", true};
TypeVar myAny{AnyTypeVar{}, /*presistent*/ true};
myAny.normal = true;
myAny.documentationSymbol = "@global/any";

View file

@ -101,4 +101,20 @@ if vector_size == 4 then
assert(vector(1, 2, 3, 4).W == 4)
end
-- negative zero should hash the same as zero
-- note: our earlier test only really checks the low hash bit, so in absence of perfect avalanche it's insufficient
do
local larget = {}
for i = 1, 2^14 do
larget[vector(0, 0, i)] = true
end
larget[vector(0, 0, 0)] = 42
assert(larget[vector(0, 0, 0)] == 42)
assert(larget[vector(0, 0, -0)] == 42)
assert(larget[vector(0, -0, 0)] == 42)
assert(larget[vector(-0, 0, 0)] == 42)
end
return 'OK'

View file

@ -137,16 +137,28 @@
<DisplayString>{data,s}</DisplayString>
</Type>
<Type Name="::CallInfo">
<Intrinsic Name="cl" Category="Property" Expression="func->value.gc->cl"/>
<Intrinsic Name="isC" Category="Property" Expression="cl().isC"/>
<Intrinsic Name="proto" Category="Property" Expression="cl().l.p"/>
<Intrinsic Name="pcRel" Category="Property" Expression="savedpc ? savedpc - proto()->code - 1 : 0"/>
<Intrinsic Name="line" Category="Property" Expression="proto()->abslineinfo[pcRel() >> proto()->linegaplog2] + proto()->lineinfo[pcRel()]"/>
<!-- Special frames -->
<DisplayString Condition="!func">empty</DisplayString>
<DisplayString Condition="func->tt != lua_Type::LUA_TFUNCTION">none</DisplayString>
<!-- Lua functions-->
<DisplayString Condition="!isC() &amp;&amp; proto()->debugname">{proto()->source->data,sb}:{line()} function {proto()->debugname->data,sb}()</DisplayString>
<DisplayString Condition="!isC()">{proto()->source->data,sb}:{line()} function()</DisplayString>
<!-- C functions-->
<DisplayString Condition="isC() &amp;&amp; cl().c.debugname">=[C] function {cl().c.debugname,sb}() {cl().c.f,na}</DisplayString>
<DisplayString Condition="isC()">=[C] {cl().c.f,na}</DisplayString>
</Type>
<Type Name ="::lua_State">
<DisplayString Condition="ci-&gt;func-&gt;value.gc-&gt;cl.isC">
{ci-&gt;func-&gt;value.gc-&gt;cl.c.f,na}
</DisplayString>
<DisplayString Condition="!ci-&gt;func-&gt;value.gc-&gt;cl.isC &amp;&amp; ci-&gt;func-&gt;value.gc-&gt;cl.l.p-&gt;debugname" Optional="true">
{ci-&gt;func-&gt;value.gc-&gt;cl.l.p-&gt;source-&gt;data,sb}:{ci-&gt;func-&gt;value.gc-&gt;cl.l.p-&gt;linedefined,d} {ci-&gt;func-&gt;value.gc-&gt;cl.l.p-&gt;debugname->data,sb}
</DisplayString>
<DisplayString Condition="!ci-&gt;func-&gt;value.gc-&gt;cl.isC" Optional="true">
{ci-&gt;func-&gt;value.gc-&gt;cl.l.p-&gt;source-&gt;data,sb}:{ci-&gt;func-&gt;value.gc-&gt;cl.l.p-&gt;linedefined,d}
</DisplayString>
<DisplayString Condition="ci">{ci,na}</DisplayString>
<DisplayString>thread</DisplayString>
<Expand>
<Synthetic Name="[call stack]">
@ -156,7 +168,7 @@
<Size>ci-base_ci</Size>
<!-- the +1 is omitted here to avoid some issues with a blank call -->
<ValueNode>
base_ci[ci-base_ci - $i].func-&gt;value.gc-&gt;cl,view(short)
base_ci[ci-base_ci - $i]
</ValueNode>
</IndexListItems>
</Expand>