Sync to upstream/release/667

This commit is contained in:
Alexander Youngblood 2025-03-28 10:07:23 -07:00
parent 2eefa3f8e0
commit d4c2c64dcd
60 changed files with 4327 additions and 2863 deletions

View file

@ -1,148 +0,0 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#pragma once
#include "Luau/AstQuery.h"
#include "Luau/Config.h"
#include "Luau/ModuleResolver.h"
#include "Luau/Scope.h"
#include "Luau/Variant.h"
#include "Luau/Normalize.h"
#include "Luau/TypePack.h"
#include "Luau/TypeArena.h"
#include <mutex>
#include <string>
#include <vector>
#include <optional>
namespace Luau
{
class AstStat;
class ParseError;
struct TypeError;
struct LintWarning;
struct GlobalTypes;
struct ModuleResolver;
struct ParseResult;
struct DcrLogger;
struct TelemetryTypePair
{
std::string annotatedType;
std::string inferredType;
};
struct AnyTypeSummary
{
TypeArena arena;
AstStatBlock* rootSrc = nullptr;
DenseHashSet<TypeId> seenTypeFamilyInstances{nullptr};
int recursionCount = 0;
std::string root;
int strictCount = 0;
DenseHashMap<const void*, bool> seen{nullptr};
AnyTypeSummary();
void traverse(const Module* module, AstStat* src, NotNull<BuiltinTypes> builtinTypes);
std::pair<bool, TypeId> checkForAnyCast(const Scope* scope, AstExprTypeAssertion* expr);
bool containsAny(TypePackId typ);
bool containsAny(TypeId typ);
bool isAnyCast(const Scope* scope, AstExpr* expr, const Module* module, NotNull<BuiltinTypes> builtinTypes);
bool isAnyCall(const Scope* scope, AstExpr* expr, const Module* module, NotNull<BuiltinTypes> builtinTypes);
bool hasVariadicAnys(const Scope* scope, AstExprFunction* expr, const Module* module, NotNull<BuiltinTypes> builtinTypes);
bool hasArgAnys(const Scope* scope, AstExprFunction* expr, const Module* module, NotNull<BuiltinTypes> builtinTypes);
bool hasAnyReturns(const Scope* scope, AstExprFunction* expr, const Module* module, NotNull<BuiltinTypes> builtinTypes);
TypeId checkForFamilyInhabitance(const TypeId instance, Location location);
TypeId lookupType(const AstExpr* expr, const Module* module, NotNull<BuiltinTypes> builtinTypes);
TypePackId reconstructTypePack(const AstArray<AstExpr*> exprs, const Module* module, NotNull<BuiltinTypes> builtinTypes);
DenseHashSet<TypeId> seenTypeFunctionInstances{nullptr};
TypeId lookupAnnotation(AstType* annotation, const Module* module, NotNull<BuiltinTypes> builtintypes);
std::optional<TypePackId> lookupPackAnnotation(AstTypePack* annotation, const Module* module);
TypeId checkForTypeFunctionInhabitance(const TypeId instance, const Location location);
enum Pattern : uint64_t
{
Casts,
FuncArg,
FuncRet,
FuncApp,
VarAnnot,
VarAny,
TableProp,
Alias,
Assign,
TypePk
};
struct TypeInfo
{
Pattern code;
std::string node;
TelemetryTypePair type;
explicit TypeInfo(Pattern code, std::string node, TelemetryTypePair type);
};
struct FindReturnAncestry final : public AstVisitor
{
AstNode* currNode{nullptr};
AstNode* stat{nullptr};
Position rootEnd;
bool found = false;
explicit FindReturnAncestry(AstNode* stat, Position rootEnd);
bool visit(AstType* node) override;
bool visit(AstNode* node) override;
bool visit(AstStatFunction* node) override;
bool visit(AstStatLocalFunction* node) override;
};
std::vector<TypeInfo> typeInfo;
/**
* Fabricates a scope that is a child of another scope.
* @param node the lexical node that the scope belongs to.
* @param parent the parent scope of the new scope. Must not be null.
*/
const Scope* childScope(const AstNode* node, const Scope* parent);
std::optional<AstExpr*> matchRequire(const AstExprCall& call);
AstNode* getNode(AstStatBlock* root, AstNode* node);
const Scope* findInnerMostScope(const Location location, const Module* module);
const AstNode* findAstAncestryAtLocation(const AstStatBlock* root, AstNode* node);
void visit(const Scope* scope, AstStat* stat, const Module* module, NotNull<BuiltinTypes> builtinTypes);
void visit(const Scope* scope, AstStatBlock* block, const Module* module, NotNull<BuiltinTypes> builtinTypes);
void visit(const Scope* scope, AstStatIf* ifStatement, const Module* module, NotNull<BuiltinTypes> builtinTypes);
void visit(const Scope* scope, AstStatWhile* while_, const Module* module, NotNull<BuiltinTypes> builtinTypes);
void visit(const Scope* scope, AstStatRepeat* repeat, const Module* module, NotNull<BuiltinTypes> builtinTypes);
void visit(const Scope* scope, AstStatReturn* ret, const Module* module, NotNull<BuiltinTypes> builtinTypes);
void visit(const Scope* scope, AstStatLocal* local, const Module* module, NotNull<BuiltinTypes> builtinTypes);
void visit(const Scope* scope, AstStatFor* for_, const Module* module, NotNull<BuiltinTypes> builtinTypes);
void visit(const Scope* scope, AstStatForIn* forIn, const Module* module, NotNull<BuiltinTypes> builtinTypes);
void visit(const Scope* scope, AstStatAssign* assign, const Module* module, NotNull<BuiltinTypes> builtinTypes);
void visit(const Scope* scope, AstStatCompoundAssign* assign, const Module* module, NotNull<BuiltinTypes> builtinTypes);
void visit(const Scope* scope, AstStatFunction* function, const Module* module, NotNull<BuiltinTypes> builtinTypes);
void visit(const Scope* scope, AstStatLocalFunction* function, const Module* module, NotNull<BuiltinTypes> builtinTypes);
void visit(const Scope* scope, AstStatTypeAlias* alias, const Module* module, NotNull<BuiltinTypes> builtinTypes);
void visit(const Scope* scope, AstStatExpr* expr, const Module* module, NotNull<BuiltinTypes> builtinTypes);
void visit(const Scope* scope, AstStatDeclareGlobal* declareGlobal, const Module* module, NotNull<BuiltinTypes> builtinTypes);
void visit(const Scope* scope, AstStatDeclareClass* declareClass, const Module* module, NotNull<BuiltinTypes> builtinTypes);
void visit(const Scope* scope, AstStatDeclareFunction* declareFunction, const Module* module, NotNull<BuiltinTypes> builtinTypes);
void visit(const Scope* scope, AstStatError* error, const Module* module, NotNull<BuiltinTypes> builtinTypes);
};
} // namespace Luau

View file

@ -50,6 +50,7 @@ struct GeneralizationConstraint
TypeId sourceType;
std::vector<TypeId> interiorTypes;
bool hasDeprecatedAttribute = false;
};
// variables ~ iterate iterator

View file

@ -365,7 +365,7 @@ public:
* @returns a non-free type that generalizes the argument, or `std::nullopt` if one
* does not exist
*/
std::optional<TypeId> generalizeFreeType(NotNull<Scope> scope, TypeId type, bool avoidSealingTables = false);
std::optional<TypeId> generalizeFreeType(NotNull<Scope> scope, TypeId type);
/**
* Checks the existing set of constraints to see if there exist any that contain

View file

@ -38,8 +38,6 @@ struct DataFlowGraph
DefId getDef(const AstExpr* expr) const;
// Look up the definition optionally, knowing it may not be present.
std::optional<DefId> getDefOptional(const AstExpr* expr) const;
// Look up for the rvalue def for a compound assignment.
std::optional<DefId> getRValueDefForCompoundAssign(const AstExpr* expr) const;
DefId getDef(const AstLocal* local) const;
@ -66,10 +64,6 @@ private:
// All keys in this maps are really only statements that ambiently declares a symbol.
DenseHashMap<const AstStat*, const Def*> declaredDefs{nullptr};
// Compound assignments are in a weird situation where the local being assigned to is also being used at its
// previous type implicitly in an rvalue position. This map provides the previous binding.
DenseHashMap<const AstExpr*, const Def*> compoundAssignDefs{nullptr};
DenseHashMap<const AstExpr*, const RefinementKey*> astRefinementKeys{nullptr};
friend struct DataFlowGraphBuilder;
};

View file

@ -49,6 +49,8 @@ struct FragmentAutocompleteAncestryResult
std::vector<AstLocal*> localStack;
std::vector<AstNode*> ancestry;
AstStat* nearestStatement = nullptr;
AstStatBlock* parentBlock = nullptr;
Location fragmentSelectionRegion;
};
struct FragmentParseResult
@ -59,6 +61,7 @@ struct FragmentParseResult
AstStat* nearestStatement = nullptr;
std::vector<Comment> commentLocations;
std::unique_ptr<Allocator> alloc = std::make_unique<Allocator>();
Position scopePos{0, 0};
};
struct FragmentTypeCheckResult
@ -76,10 +79,28 @@ struct FragmentAutocompleteResult
AutocompleteResult acResults;
};
FragmentAutocompleteAncestryResult findAncestryForFragmentParse(AstStatBlock* root, const Position& cursorPos);
struct FragmentRegion
{
Location fragmentLocation;
AstStat* nearestStatement = nullptr; // used for tests
AstStatBlock* parentBlock = nullptr; // used for scope detection
};
FragmentRegion getFragmentRegion(AstStatBlock* root, const Position& cursorPosition);
FragmentAutocompleteAncestryResult findAncestryForFragmentParse(AstStatBlock* stale, const Position& cursorPos, AstStatBlock* lastGoodParse);
FragmentAutocompleteAncestryResult findAncestryForFragmentParse_DEPRECATED(AstStatBlock* root, const Position& cursorPos);
std::optional<FragmentParseResult> parseFragment_DEPRECATED(
AstStatBlock* root,
AstNameTable* names,
std::string_view src,
const Position& cursorPos,
std::optional<Position> fragmentEndPosition
);
std::optional<FragmentParseResult> parseFragment(
AstStatBlock* root,
AstStatBlock* stale,
AstStatBlock* mostRecentParse,
AstNameTable* names,
std::string_view src,
const Position& cursorPos,
@ -93,6 +114,7 @@ std::pair<FragmentTypeCheckStatus, FragmentTypeCheckResult> typecheckFragment(
std::optional<FrontendOptions> opts,
std::string_view src,
std::optional<Position> fragmentEndPosition,
AstStatBlock* recentParse = nullptr,
IFragmentAutocompleteReporter* reporter = nullptr
);
@ -104,6 +126,7 @@ FragmentAutocompleteResult fragmentAutocomplete(
std::optional<FrontendOptions> opts,
StringCompletionCallback callback,
std::optional<Position> fragmentEndPosition = std::nullopt,
AstStatBlock* recentParse = nullptr,
IFragmentAutocompleteReporter* reporter = nullptr
);

View file

@ -10,7 +10,6 @@
#include "Luau/Set.h"
#include "Luau/TypeCheckLimits.h"
#include "Luau/Variant.h"
#include "Luau/AnyTypeSummary.h"
#include <mutex>
#include <string>
@ -34,7 +33,6 @@ struct HotComment;
struct BuildQueueItem;
struct BuildQueueWorkState;
struct FrontendCancellationToken;
struct AnyTypeSummary;
struct LoadDefinitionFileResult
{

View file

@ -13,8 +13,7 @@ std::optional<TypeId> generalize(
NotNull<BuiltinTypes> builtinTypes,
NotNull<Scope> scope,
NotNull<DenseHashSet<TypeId>> cachedTypes,
TypeId ty,
/* avoid sealing tables*/ bool avoidSealingTables = false
TypeId ty
);
}

View file

@ -8,7 +8,6 @@
#include "Luau/ParseResult.h"
#include "Luau/Scope.h"
#include "Luau/TypeArena.h"
#include "Luau/AnyTypeSummary.h"
#include "Luau/DataFlowGraph.h"
#include <memory>
@ -21,14 +20,13 @@ LUAU_FASTFLAG(LuauIncrementalAutocompleteCommentDetection)
namespace Luau
{
using LogLuauProc = void (*)(std::string_view);
using LogLuauProc = void (*)(std::string_view, std::string_view);
extern LogLuauProc logLuau;
void setLogLuau(LogLuauProc ll);
void resetLogLuauProc();
struct Module;
struct AnyTypeSummary;
using ScopePtr = std::shared_ptr<struct Scope>;
using ModulePtr = std::shared_ptr<Module>;
@ -86,10 +84,6 @@ struct Module
TypeArena interfaceTypes;
TypeArena internalTypes;
// Summary of Ast Nodes that either contain
// user annotated anys or typechecker inferred anys
AnyTypeSummary ats{};
// Scopes and AST types refer to parse data, so we need to keep that alive
std::shared_ptr<Allocator> allocator;
std::shared_ptr<AstNameTable> names;

View file

@ -1,9 +1,10 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#pragma once
#include "Luau/DataFlowGraph.h"
#include "Luau/EqSatSimplification.h"
#include "Luau/Module.h"
#include "Luau/NotNull.h"
#include "Luau/DataFlowGraph.h"
namespace Luau
{

View file

@ -19,7 +19,6 @@
#include <optional>
#include <set>
#include <string>
#include <unordered_map>
#include <vector>
LUAU_FASTINT(LuauTableTypeMaximumStringifierLength)
@ -38,6 +37,15 @@ struct Constraint;
struct Subtyping;
struct TypeChecker2;
enum struct Polarity : uint8_t
{
None = 0b000,
Positive = 0b001,
Negative = 0b010,
Mixed = 0b011,
Unknown = 0b100,
};
/**
* There are three kinds of type variables:
* - `Free` variables are metavariables, which stand for unconstrained types.
@ -396,6 +404,7 @@ struct FunctionType
// this flag is used as an optimization to exit early from procedures that manipulate free or generic types.
bool hasNoFreeOrGenericTypes = false;
bool isCheckedFunction = false;
bool isDeprecatedFunction = false;
};
enum class TableState

View file

@ -177,6 +177,7 @@ struct FunctionGraphReductionResult
DenseHashSet<TypePackId> blockedPacks{nullptr};
DenseHashSet<TypeId> reducedTypes{nullptr};
DenseHashSet<TypePackId> reducedPacks{nullptr};
DenseHashSet<TypeId> irreducibleTypes{nullptr};
};
/**

View file

@ -1,902 +0,0 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#include "Luau/AnyTypeSummary.h"
#include "Luau/BuiltinDefinitions.h"
#include "Luau/Clone.h"
#include "Luau/Common.h"
#include "Luau/Config.h"
#include "Luau/ConstraintGenerator.h"
#include "Luau/ConstraintSolver.h"
#include "Luau/DataFlowGraph.h"
#include "Luau/DcrLogger.h"
#include "Luau/Module.h"
#include "Luau/Parser.h"
#include "Luau/Scope.h"
#include "Luau/StringUtils.h"
#include "Luau/TimeTrace.h"
#include "Luau/ToString.h"
#include "Luau/Transpiler.h"
#include "Luau/TypeArena.h"
#include "Luau/TypeChecker2.h"
#include "Luau/NonStrictTypeChecker.h"
#include "Luau/TypeInfer.h"
#include "Luau/Variant.h"
#include "Luau/VisitType.h"
#include "Luau/TypePack.h"
#include "Luau/TypeOrPack.h"
#include <algorithm>
#include <memory>
#include <chrono>
#include <condition_variable>
#include <exception>
#include <mutex>
#include <stdexcept>
#include <string>
#include <iostream>
#include <stdio.h>
LUAU_FASTFLAGVARIABLE(StudioReportLuauAny2);
LUAU_FASTINTVARIABLE(LuauAnySummaryRecursionLimit, 300);
LUAU_FASTFLAG(DebugLuauMagicTypes);
namespace Luau
{
void AnyTypeSummary::traverse(const Module* module, AstStat* src, NotNull<BuiltinTypes> builtinTypes)
{
visit(findInnerMostScope(src->location, module), src, module, builtinTypes);
}
void AnyTypeSummary::visit(const Scope* scope, AstStat* stat, const Module* module, NotNull<BuiltinTypes> builtinTypes)
{
RecursionLimiter limiter{&recursionCount, FInt::LuauAnySummaryRecursionLimit};
if (auto s = stat->as<AstStatBlock>())
return visit(scope, s, module, builtinTypes);
else if (auto i = stat->as<AstStatIf>())
return visit(scope, i, module, builtinTypes);
else if (auto s = stat->as<AstStatWhile>())
return visit(scope, s, module, builtinTypes);
else if (auto s = stat->as<AstStatRepeat>())
return visit(scope, s, module, builtinTypes);
else if (auto r = stat->as<AstStatReturn>())
return visit(scope, r, module, builtinTypes);
else if (auto e = stat->as<AstStatExpr>())
return visit(scope, e, module, builtinTypes);
else if (auto s = stat->as<AstStatLocal>())
return visit(scope, s, module, builtinTypes);
else if (auto s = stat->as<AstStatFor>())
return visit(scope, s, module, builtinTypes);
else if (auto s = stat->as<AstStatForIn>())
return visit(scope, s, module, builtinTypes);
else if (auto a = stat->as<AstStatAssign>())
return visit(scope, a, module, builtinTypes);
else if (auto a = stat->as<AstStatCompoundAssign>())
return visit(scope, a, module, builtinTypes);
else if (auto f = stat->as<AstStatFunction>())
return visit(scope, f, module, builtinTypes);
else if (auto f = stat->as<AstStatLocalFunction>())
return visit(scope, f, module, builtinTypes);
else if (auto a = stat->as<AstStatTypeAlias>())
return visit(scope, a, module, builtinTypes);
else if (auto s = stat->as<AstStatDeclareGlobal>())
return visit(scope, s, module, builtinTypes);
else if (auto s = stat->as<AstStatDeclareFunction>())
return visit(scope, s, module, builtinTypes);
else if (auto s = stat->as<AstStatDeclareClass>())
return visit(scope, s, module, builtinTypes);
else if (auto s = stat->as<AstStatError>())
return visit(scope, s, module, builtinTypes);
}
void AnyTypeSummary::visit(const Scope* scope, AstStatBlock* block, const Module* module, NotNull<BuiltinTypes> builtinTypes)
{
RecursionCounter counter{&recursionCount};
if (recursionCount >= FInt::LuauAnySummaryRecursionLimit)
return; // don't report
for (AstStat* stat : block->body)
visit(scope, stat, module, builtinTypes);
}
void AnyTypeSummary::visit(const Scope* scope, AstStatIf* ifStatement, const Module* module, NotNull<BuiltinTypes> builtinTypes)
{
if (ifStatement->thenbody)
{
const Scope* thenScope = findInnerMostScope(ifStatement->thenbody->location, module);
visit(thenScope, ifStatement->thenbody, module, builtinTypes);
}
if (ifStatement->elsebody)
{
const Scope* elseScope = findInnerMostScope(ifStatement->elsebody->location, module);
visit(elseScope, ifStatement->elsebody, module, builtinTypes);
}
}
void AnyTypeSummary::visit(const Scope* scope, AstStatWhile* while_, const Module* module, NotNull<BuiltinTypes> builtinTypes)
{
const Scope* whileScope = findInnerMostScope(while_->location, module);
visit(whileScope, while_->body, module, builtinTypes);
}
void AnyTypeSummary::visit(const Scope* scope, AstStatRepeat* repeat, const Module* module, NotNull<BuiltinTypes> builtinTypes)
{
const Scope* repeatScope = findInnerMostScope(repeat->location, module);
visit(repeatScope, repeat->body, module, builtinTypes);
}
void AnyTypeSummary::visit(const Scope* scope, AstStatReturn* ret, const Module* module, NotNull<BuiltinTypes> builtinTypes)
{
const Scope* retScope = findInnerMostScope(ret->location, module);
auto ctxNode = getNode(rootSrc, ret);
bool seenTP = false;
for (auto val : ret->list)
{
if (isAnyCall(retScope, val, module, builtinTypes))
{
TelemetryTypePair types;
types.inferredType = toString(lookupType(val, module, builtinTypes));
TypeInfo ti{Pattern::FuncApp, toString(ctxNode), types};
typeInfo.push_back(ti);
}
if (isAnyCast(retScope, val, module, builtinTypes))
{
if (auto cast = val->as<AstExprTypeAssertion>())
{
TelemetryTypePair types;
types.annotatedType = toString(lookupAnnotation(cast->annotation, module, builtinTypes));
types.inferredType = toString(lookupType(cast->expr, module, builtinTypes));
TypeInfo ti{Pattern::Casts, toString(ctxNode), types};
typeInfo.push_back(ti);
}
}
if (ret->list.size > 1 && !seenTP)
{
if (containsAny(retScope->returnType))
{
seenTP = true;
TelemetryTypePair types;
types.inferredType = toString(retScope->returnType);
TypeInfo ti{Pattern::TypePk, toString(ctxNode), types};
typeInfo.push_back(ti);
}
}
}
}
void AnyTypeSummary::visit(const Scope* scope, AstStatLocal* local, const Module* module, NotNull<BuiltinTypes> builtinTypes)
{
auto ctxNode = getNode(rootSrc, local);
TypePackId values = reconstructTypePack(local->values, module, builtinTypes);
auto [head, tail] = flatten(values);
size_t posn = 0;
for (AstLocal* loc : local->vars)
{
if (local->vars.data[0] == loc && posn < local->values.size)
{
if (loc->annotation)
{
auto annot = lookupAnnotation(loc->annotation, module, builtinTypes);
if (containsAny(annot))
{
TelemetryTypePair types;
types.annotatedType = toString(annot);
types.inferredType = toString(lookupType(local->values.data[posn], module, builtinTypes));
TypeInfo ti{Pattern::VarAnnot, toString(ctxNode), types};
typeInfo.push_back(ti);
}
}
const AstExprTypeAssertion* maybeRequire = local->values.data[posn]->as<AstExprTypeAssertion>();
if (!maybeRequire)
continue;
if (std::min(local->values.size - 1, posn) < head.size())
{
if (isAnyCast(scope, local->values.data[posn], module, builtinTypes))
{
TelemetryTypePair types;
types.inferredType = toString(head[std::min(local->values.size - 1, posn)]);
TypeInfo ti{Pattern::Casts, toString(ctxNode), types};
typeInfo.push_back(ti);
}
}
}
else
{
if (std::min(local->values.size - 1, posn) < head.size())
{
if (loc->annotation)
{
auto annot = lookupAnnotation(loc->annotation, module, builtinTypes);
if (containsAny(annot))
{
TelemetryTypePair types;
types.annotatedType = toString(annot);
types.inferredType = toString(head[std::min(local->values.size - 1, posn)]);
TypeInfo ti{Pattern::VarAnnot, toString(ctxNode), types};
typeInfo.push_back(ti);
}
}
}
else
{
if (tail)
{
if (containsAny(*tail))
{
TelemetryTypePair types;
types.inferredType = toString(*tail);
TypeInfo ti{Pattern::VarAny, toString(ctxNode), types};
typeInfo.push_back(ti);
}
}
}
}
++posn;
}
}
void AnyTypeSummary::visit(const Scope* scope, AstStatFor* for_, const Module* module, NotNull<BuiltinTypes> builtinTypes)
{
const Scope* forScope = findInnerMostScope(for_->location, module);
visit(forScope, for_->body, module, builtinTypes);
}
void AnyTypeSummary::visit(const Scope* scope, AstStatForIn* forIn, const Module* module, NotNull<BuiltinTypes> builtinTypes)
{
const Scope* loopScope = findInnerMostScope(forIn->location, module);
visit(loopScope, forIn->body, module, builtinTypes);
}
void AnyTypeSummary::visit(const Scope* scope, AstStatAssign* assign, const Module* module, NotNull<BuiltinTypes> builtinTypes)
{
auto ctxNode = getNode(rootSrc, assign);
TypePackId values = reconstructTypePack(assign->values, module, builtinTypes);
auto [head, tail] = flatten(values);
size_t posn = 0;
for (AstExpr* var : assign->vars)
{
TypeId tp = lookupType(var, module, builtinTypes);
if (containsAny(tp))
{
TelemetryTypePair types;
types.annotatedType = toString(tp);
auto loc = std::min(assign->vars.size - 1, posn);
if (head.size() >= assign->vars.size && posn < head.size())
{
types.inferredType = toString(head[posn]);
}
else if (loc < head.size())
types.inferredType = toString(head[loc]);
else
types.inferredType = toString(builtinTypes->nilType);
TypeInfo ti{Pattern::Assign, toString(ctxNode), types};
typeInfo.push_back(ti);
}
++posn;
}
for (AstExpr* val : assign->values)
{
if (isAnyCall(scope, val, module, builtinTypes))
{
TelemetryTypePair types;
types.inferredType = toString(lookupType(val, module, builtinTypes));
TypeInfo ti{Pattern::FuncApp, toString(ctxNode), types};
typeInfo.push_back(ti);
}
if (isAnyCast(scope, val, module, builtinTypes))
{
if (auto cast = val->as<AstExprTypeAssertion>())
{
TelemetryTypePair types;
types.annotatedType = toString(lookupAnnotation(cast->annotation, module, builtinTypes));
types.inferredType = toString(lookupType(val, module, builtinTypes));
TypeInfo ti{Pattern::Casts, toString(ctxNode), types};
typeInfo.push_back(ti);
}
}
}
if (tail)
{
if (containsAny(*tail))
{
TelemetryTypePair types;
types.inferredType = toString(*tail);
TypeInfo ti{Pattern::Assign, toString(ctxNode), types};
typeInfo.push_back(ti);
}
}
}
void AnyTypeSummary::visit(const Scope* scope, AstStatCompoundAssign* assign, const Module* module, NotNull<BuiltinTypes> builtinTypes)
{
auto ctxNode = getNode(rootSrc, assign);
TelemetryTypePair types;
types.inferredType = toString(lookupType(assign->value, module, builtinTypes));
types.annotatedType = toString(lookupType(assign->var, module, builtinTypes));
if (module->astTypes.contains(assign->var))
{
if (containsAny(*module->astTypes.find(assign->var)))
{
TypeInfo ti{Pattern::Assign, toString(ctxNode), types};
typeInfo.push_back(ti);
}
}
else if (module->astTypePacks.contains(assign->var))
{
if (containsAny(*module->astTypePacks.find(assign->var)))
{
TypeInfo ti{Pattern::Assign, toString(ctxNode), types};
typeInfo.push_back(ti);
}
}
if (isAnyCall(scope, assign->value, module, builtinTypes))
{
TypeInfo ti{Pattern::FuncApp, toString(ctxNode), types};
typeInfo.push_back(ti);
}
if (isAnyCast(scope, assign->value, module, builtinTypes))
{
if (auto cast = assign->value->as<AstExprTypeAssertion>())
{
types.annotatedType = toString(lookupAnnotation(cast->annotation, module, builtinTypes));
types.inferredType = toString(lookupType(cast->expr, module, builtinTypes));
TypeInfo ti{Pattern::Casts, toString(ctxNode), types};
typeInfo.push_back(ti);
}
}
}
void AnyTypeSummary::visit(const Scope* scope, AstStatFunction* function, const Module* module, NotNull<BuiltinTypes> builtinTypes)
{
TelemetryTypePair types;
types.inferredType = toString(lookupType(function->func, module, builtinTypes));
if (hasVariadicAnys(scope, function->func, module, builtinTypes))
{
TypeInfo ti{Pattern::VarAny, toString(function), types};
typeInfo.push_back(ti);
}
if (hasArgAnys(scope, function->func, module, builtinTypes))
{
TypeInfo ti{Pattern::FuncArg, toString(function), types};
typeInfo.push_back(ti);
}
if (hasAnyReturns(scope, function->func, module, builtinTypes))
{
TypeInfo ti{Pattern::FuncRet, toString(function), types};
typeInfo.push_back(ti);
}
if (function->func->body->body.size > 0)
visit(scope, function->func->body, module, builtinTypes);
}
void AnyTypeSummary::visit(const Scope* scope, AstStatLocalFunction* function, const Module* module, NotNull<BuiltinTypes> builtinTypes)
{
TelemetryTypePair types;
if (hasVariadicAnys(scope, function->func, module, builtinTypes))
{
types.inferredType = toString(lookupType(function->func, module, builtinTypes));
TypeInfo ti{Pattern::VarAny, toString(function), types};
typeInfo.push_back(ti);
}
if (hasArgAnys(scope, function->func, module, builtinTypes))
{
types.inferredType = toString(lookupType(function->func, module, builtinTypes));
TypeInfo ti{Pattern::FuncArg, toString(function), types};
typeInfo.push_back(ti);
}
if (hasAnyReturns(scope, function->func, module, builtinTypes))
{
types.inferredType = toString(lookupType(function->func, module, builtinTypes));
TypeInfo ti{Pattern::FuncRet, toString(function), types};
typeInfo.push_back(ti);
}
if (function->func->body->body.size > 0)
visit(scope, function->func->body, module, builtinTypes);
}
void AnyTypeSummary::visit(const Scope* scope, AstStatTypeAlias* alias, const Module* module, NotNull<BuiltinTypes> builtinTypes)
{
auto ctxNode = getNode(rootSrc, alias);
auto annot = lookupAnnotation(alias->type, module, builtinTypes);
if (containsAny(annot))
{
// no expr => no inference for aliases
TelemetryTypePair types;
types.annotatedType = toString(annot);
TypeInfo ti{Pattern::Alias, toString(ctxNode), types};
typeInfo.push_back(ti);
}
}
void AnyTypeSummary::visit(const Scope* scope, AstStatExpr* expr, const Module* module, NotNull<BuiltinTypes> builtinTypes)
{
auto ctxNode = getNode(rootSrc, expr);
if (isAnyCall(scope, expr->expr, module, builtinTypes))
{
TelemetryTypePair types;
types.inferredType = toString(lookupType(expr->expr, module, builtinTypes));
TypeInfo ti{Pattern::FuncApp, toString(ctxNode), types};
typeInfo.push_back(ti);
}
}
void AnyTypeSummary::visit(const Scope* scope, AstStatDeclareGlobal* declareGlobal, const Module* module, NotNull<BuiltinTypes> builtinTypes) {}
void AnyTypeSummary::visit(const Scope* scope, AstStatDeclareClass* declareClass, const Module* module, NotNull<BuiltinTypes> builtinTypes) {}
void AnyTypeSummary::visit(const Scope* scope, AstStatDeclareFunction* declareFunction, const Module* module, NotNull<BuiltinTypes> builtinTypes) {}
void AnyTypeSummary::visit(const Scope* scope, AstStatError* error, const Module* module, NotNull<BuiltinTypes> builtinTypes) {}
TypeId AnyTypeSummary::checkForFamilyInhabitance(const TypeId instance, const Location location)
{
if (seenTypeFamilyInstances.find(instance))
return instance;
seenTypeFamilyInstances.insert(instance);
return instance;
}
TypeId AnyTypeSummary::lookupType(const AstExpr* expr, const Module* module, NotNull<BuiltinTypes> builtinTypes)
{
const TypeId* ty = module->astTypes.find(expr);
if (ty)
return checkForFamilyInhabitance(follow(*ty), expr->location);
const TypePackId* tp = module->astTypePacks.find(expr);
if (tp)
{
if (auto fst = first(*tp, /*ignoreHiddenVariadics*/ false))
return checkForFamilyInhabitance(*fst, expr->location);
else if (finite(*tp) && size(*tp) == 0)
return checkForFamilyInhabitance(builtinTypes->nilType, expr->location);
}
return builtinTypes->errorRecoveryType();
}
TypePackId AnyTypeSummary::reconstructTypePack(AstArray<AstExpr*> exprs, const Module* module, NotNull<BuiltinTypes> builtinTypes)
{
if (exprs.size == 0)
return arena.addTypePack(TypePack{{}, std::nullopt});
std::vector<TypeId> head;
for (size_t i = 0; i < exprs.size - 1; ++i)
{
head.push_back(lookupType(exprs.data[i], module, builtinTypes));
}
const TypePackId* tail = module->astTypePacks.find(exprs.data[exprs.size - 1]);
if (tail)
return arena.addTypePack(TypePack{std::move(head), follow(*tail)});
else
return arena.addTypePack(TypePack{std::move(head), builtinTypes->errorRecoveryTypePack()});
}
bool AnyTypeSummary::isAnyCall(const Scope* scope, AstExpr* expr, const Module* module, NotNull<BuiltinTypes> builtinTypes)
{
if (auto call = expr->as<AstExprCall>())
{
TypePackId args = reconstructTypePack(call->args, module, builtinTypes);
if (containsAny(args))
return true;
TypeId func = lookupType(call->func, module, builtinTypes);
if (containsAny(func))
return true;
}
return false;
}
bool AnyTypeSummary::hasVariadicAnys(const Scope* scope, AstExprFunction* expr, const Module* module, NotNull<BuiltinTypes> builtinTypes)
{
if (expr->vararg && expr->varargAnnotation)
{
auto annot = lookupPackAnnotation(expr->varargAnnotation, module);
if (annot && containsAny(*annot))
{
return true;
}
}
return false;
}
bool AnyTypeSummary::hasArgAnys(const Scope* scope, AstExprFunction* expr, const Module* module, NotNull<BuiltinTypes> builtinTypes)
{
if (expr->args.size > 0)
{
for (const AstLocal* arg : expr->args)
{
if (arg->annotation)
{
auto annot = lookupAnnotation(arg->annotation, module, builtinTypes);
if (containsAny(annot))
{
return true;
}
}
}
}
return false;
}
bool AnyTypeSummary::hasAnyReturns(const Scope* scope, AstExprFunction* expr, const Module* module, NotNull<BuiltinTypes> builtinTypes)
{
if (!expr->returnAnnotation)
{
return false;
}
for (AstType* ret : expr->returnAnnotation->types)
{
if (containsAny(lookupAnnotation(ret, module, builtinTypes)))
{
return true;
}
}
if (expr->returnAnnotation->tailType)
{
auto annot = lookupPackAnnotation(expr->returnAnnotation->tailType, module);
if (annot && containsAny(*annot))
{
return true;
}
}
return false;
}
bool AnyTypeSummary::isAnyCast(const Scope* scope, AstExpr* expr, const Module* module, NotNull<BuiltinTypes> builtinTypes)
{
if (auto cast = expr->as<AstExprTypeAssertion>())
{
auto annot = lookupAnnotation(cast->annotation, module, builtinTypes);
if (containsAny(annot))
{
return true;
}
}
return false;
}
TypeId AnyTypeSummary::lookupAnnotation(AstType* annotation, const Module* module, NotNull<BuiltinTypes> builtintypes)
{
if (FFlag::DebugLuauMagicTypes)
{
if (auto ref = annotation->as<AstTypeReference>(); ref && ref->parameters.size > 0)
{
if (auto ann = ref->parameters.data[0].type)
{
TypeId argTy = lookupAnnotation(ref->parameters.data[0].type, module, builtintypes);
return follow(argTy);
}
}
}
const TypeId* ty = module->astResolvedTypes.find(annotation);
if (ty)
return checkForTypeFunctionInhabitance(follow(*ty), annotation->location);
else
return checkForTypeFunctionInhabitance(builtintypes->errorRecoveryType(), annotation->location);
}
TypeId AnyTypeSummary::checkForTypeFunctionInhabitance(const TypeId instance, const Location location)
{
if (seenTypeFunctionInstances.find(instance))
return instance;
seenTypeFunctionInstances.insert(instance);
return instance;
}
std::optional<TypePackId> AnyTypeSummary::lookupPackAnnotation(AstTypePack* annotation, const Module* module)
{
const TypePackId* tp = module->astResolvedTypePacks.find(annotation);
if (tp != nullptr)
return {follow(*tp)};
return {};
}
bool AnyTypeSummary::containsAny(TypeId typ)
{
typ = follow(typ);
if (auto t = seen.find(typ); t && !*t)
{
return false;
}
seen[typ] = false;
RecursionCounter counter{&recursionCount};
if (recursionCount >= FInt::LuauAnySummaryRecursionLimit)
{
return false;
}
bool found = false;
if (auto ty = get<AnyType>(typ))
{
found = true;
}
else if (auto ty = get<UnknownType>(typ))
{
found = true;
}
else if (auto ty = get<TableType>(typ))
{
for (auto& [_name, prop] : ty->props)
{
if (FFlag::LuauSolverV2)
{
if (auto newT = follow(prop.readTy))
{
if (containsAny(*newT))
found = true;
}
else if (auto newT = follow(prop.writeTy))
{
if (containsAny(*newT))
found = true;
}
}
else
{
if (containsAny(prop.type()))
found = true;
}
}
}
else if (auto ty = get<IntersectionType>(typ))
{
for (auto part : ty->parts)
{
if (containsAny(part))
{
found = true;
}
}
}
else if (auto ty = get<UnionType>(typ))
{
for (auto option : ty->options)
{
if (containsAny(option))
{
found = true;
}
}
}
else if (auto ty = get<FunctionType>(typ))
{
if (containsAny(ty->argTypes))
found = true;
else if (containsAny(ty->retTypes))
found = true;
}
seen[typ] = found;
return found;
}
bool AnyTypeSummary::containsAny(TypePackId typ)
{
typ = follow(typ);
if (auto t = seen.find(typ); t && !*t)
{
return false;
}
seen[typ] = false;
auto [head, tail] = flatten(typ);
bool found = false;
for (auto tp : head)
{
if (containsAny(tp))
found = true;
}
if (tail)
{
if (auto vtp = get<VariadicTypePack>(tail))
{
if (auto ty = get<AnyType>(follow(vtp->ty)))
{
found = true;
}
}
else if (auto tftp = get<TypeFunctionInstanceTypePack>(tail))
{
for (TypePackId tp : tftp->packArguments)
{
if (containsAny(tp))
{
found = true;
}
}
for (TypeId t : tftp->typeArguments)
{
if (containsAny(t))
{
found = true;
}
}
}
}
seen[typ] = found;
return found;
}
const Scope* AnyTypeSummary::findInnerMostScope(const Location location, const Module* module)
{
const Scope* bestScope = module->getModuleScope().get();
bool didNarrow = false;
do
{
didNarrow = false;
for (auto scope : bestScope->children)
{
if (scope->location.encloses(location))
{
bestScope = scope.get();
didNarrow = true;
break;
}
}
} while (didNarrow && bestScope->children.size() > 0);
return bestScope;
}
std::optional<AstExpr*> AnyTypeSummary::matchRequire(const AstExprCall& call)
{
const char* require = "require";
if (call.args.size != 1)
return std::nullopt;
const AstExprGlobal* funcAsGlobal = call.func->as<AstExprGlobal>();
if (!funcAsGlobal || funcAsGlobal->name != require)
return std::nullopt;
if (call.args.size != 1)
return std::nullopt;
return call.args.data[0];
}
AstNode* AnyTypeSummary::getNode(AstStatBlock* root, AstNode* node)
{
FindReturnAncestry finder(node, root->location.end);
root->visit(&finder);
if (!finder.currNode)
finder.currNode = node;
LUAU_ASSERT(finder.found && finder.currNode);
return finder.currNode;
}
bool AnyTypeSummary::FindReturnAncestry::visit(AstStatLocalFunction* node)
{
currNode = node;
return !found;
}
bool AnyTypeSummary::FindReturnAncestry::visit(AstStatFunction* node)
{
currNode = node;
return !found;
}
bool AnyTypeSummary::FindReturnAncestry::visit(AstType* node)
{
return !found;
}
bool AnyTypeSummary::FindReturnAncestry::visit(AstNode* node)
{
if (node == stat)
{
found = true;
}
if (node->location.end == rootEnd && stat->location.end >= rootEnd)
{
currNode = node;
found = true;
}
return !found;
}
AnyTypeSummary::TypeInfo::TypeInfo(Pattern code, std::string node, TelemetryTypePair type)
: code(code)
, node(node)
, type(type)
{
}
AnyTypeSummary::FindReturnAncestry::FindReturnAncestry(AstNode* stat, Position rootEnd)
: stat(stat)
, rootEnd(rootEnd)
{
}
AnyTypeSummary::AnyTypeSummary() {}
} // namespace Luau

View file

@ -1151,6 +1151,8 @@ struct AstJsonEncoder : public AstVisitor
return writeString("checked");
case AstAttr::Type::Native:
return writeString("native");
case AstAttr::Type::Deprecated:
return writeString("deprecated");
}
}

View file

@ -33,6 +33,7 @@ LUAU_FASTINT(LuauCheckRecursionLimit)
LUAU_FASTFLAG(DebugLuauLogSolverToJson)
LUAU_FASTFLAG(DebugLuauMagicTypes)
LUAU_FASTFLAG(LuauPreserveUnionIntersectionNodeForLeadingTokenSingleType)
LUAU_FASTFLAGVARIABLE(LuauPropagateExpectedTypesForCalls)
LUAU_FASTFLAG(DebugLuauGreedyGeneralization)
LUAU_FASTFLAGVARIABLE(LuauTrackInteriorFreeTypesOnScope)
@ -46,6 +47,8 @@ LUAU_FASTFLAGVARIABLE(LuauDoNotLeakNilInRefinement)
LUAU_FASTFLAGVARIABLE(LuauExtraFollows)
LUAU_FASTFLAG(LuauUserTypeFunTypecheck)
LUAU_FASTFLAG(LuauDeprecatedAttribute)
namespace Luau
{
@ -547,7 +550,7 @@ void ConstraintGenerator::computeRefinement(
refis->get(proposition->key->def)->shouldAppendNilType =
(sense || !eq) && containsSubscriptedDefinition(proposition->key->def) && !proposition->implicitFromCall;
}
else
else
{
refis->get(proposition->key->def)->shouldAppendNilType = (sense || !eq) && containsSubscriptedDefinition(proposition->key->def);
}
@ -1357,6 +1360,23 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatRepeat* rep
return ControlFlow::None;
}
static void propagateDeprecatedAttributeToConstraint(ConstraintV& c, const AstExprFunction* func)
{
LUAU_ASSERT(FFlag::LuauDeprecatedAttribute);
if (GeneralizationConstraint* genConstraint = c.get_if<GeneralizationConstraint>())
{
genConstraint->hasDeprecatedAttribute = func->hasAttribute(AstAttr::Type::Deprecated);
}
}
static void propagateDeprecatedAttributeToType(TypeId signature, const AstExprFunction* func)
{
LUAU_ASSERT(FFlag::LuauDeprecatedAttribute);
FunctionType* fty = getMutable<FunctionType>(signature);
LUAU_ASSERT(fty);
fty->isDeprecatedFunction = func->hasAttribute(AstAttr::Type::Deprecated);
}
ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatLocalFunction* function)
{
// Local
@ -1394,6 +1414,9 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatLocalFuncti
std::unique_ptr<Constraint> c =
std::make_unique<Constraint>(constraintScope, function->name->location, GeneralizationConstraint{functionType, sig.signature});
if (FFlag::LuauDeprecatedAttribute)
propagateDeprecatedAttributeToConstraint(c->c, function->func);
Constraint* previous = nullptr;
forEachConstraint(
start,
@ -1417,7 +1440,11 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatLocalFuncti
module->astTypes[function->func] = functionType;
}
else
{
module->astTypes[function->func] = sig.signature;
if (FFlag::LuauDeprecatedAttribute)
propagateDeprecatedAttributeToType(sig.signature, function->func);
}
return ControlFlow::None;
}
@ -1458,7 +1485,11 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatFunction* f
TypeId generalizedType = arena->addType(BlockedType{});
if (sigFullyDefined)
{
emplaceType<BoundType>(asMutable(generalizedType), sig.signature);
if (FFlag::LuauDeprecatedAttribute)
propagateDeprecatedAttributeToType(sig.signature, function->func);
}
else
{
const ScopePtr& constraintScope = sig.signatureScope ? sig.signatureScope : sig.bodyScope;
@ -1466,6 +1497,9 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatFunction* f
NotNull<Constraint> c = addConstraint(constraintScope, function->name->location, GeneralizationConstraint{generalizedType, sig.signature});
getMutable<BlockedType>(generalizedType)->setOwner(c);
if (FFlag::LuauDeprecatedAttribute)
propagateDeprecatedAttributeToConstraint(c->c, function->func);
Constraint* previous = nullptr;
forEachConstraint(
start,
@ -1981,6 +2015,8 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatDeclareFunc
TypeId fnType = arena->addType(FunctionType{TypeLevel{}, funScope.get(), std::move(genericTys), std::move(genericTps), paramPack, retPack, defn});
FunctionType* ftv = getMutable<FunctionType>(fnType);
ftv->isCheckedFunction = global->isCheckedFunction();
if (FFlag::LuauDeprecatedAttribute)
ftv->isDeprecatedFunction = global->hasAttribute(AstAttr::Type::Deprecated);
ftv->argNames.reserve(global->paramNames.size);
for (const auto& el : global->paramNames)
@ -2148,13 +2184,23 @@ InferencePack ConstraintGenerator::checkPack(const ScopePtr& scope, AstExprCall*
}
else if (i < exprArgs.size() - 1 || !(arg->is<AstExprCall>() || arg->is<AstExprVarargs>()))
{
auto [ty, refinement] = check(scope, arg, /*expectedType*/ std::nullopt, /*forceSingleton*/ false, /*generalize*/ false);
std::optional<TypeId> expectedType = std::nullopt;
if (FFlag::LuauPropagateExpectedTypesForCalls && i < expectedTypesForCall.size())
{
expectedType = expectedTypesForCall[i];
}
auto [ty, refinement] = check(scope, arg, expectedType, /*forceSingleton*/ false, /*generalize*/ false);
args.push_back(ty);
argumentRefinements.push_back(refinement);
}
else
{
auto [tp, refis] = checkPack(scope, arg, {});
std::vector<std::optional<Luau::TypeId>> expectedTypes = {};
if (FFlag::LuauPropagateExpectedTypesForCalls && i < expectedTypesForCall.size())
{
expectedTypes.insert(expectedTypes.end(), expectedTypesForCall.begin() + int(i), expectedTypesForCall.end());
}
auto [tp, refis] = checkPack(scope, arg, expectedTypes);
argTail = tp;
argumentRefinements.insert(argumentRefinements.end(), refis.begin(), refis.end());
}
@ -2414,8 +2460,7 @@ Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprConstantBool*
Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprLocal* local)
{
const RefinementKey* key = dfg->getRefinementKey(local);
std::optional<DefId> rvalueDef = dfg->getRValueDefForCompoundAssign(local);
LUAU_ASSERT(key || rvalueDef);
LUAU_ASSERT(key);
std::optional<TypeId> maybeTy;
@ -2423,11 +2468,6 @@ Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprLocal* local)
if (key)
maybeTy = lookup(scope, local->location, key->def);
// if the current def doesn't have a type, we might be doing a compound assignment
// and therefore might need to look at the rvalue def instead.
if (!maybeTy && rvalueDef)
maybeTy = lookup(scope, local->location, *rvalueDef);
if (maybeTy)
{
TypeId ty = follow(*maybeTy);
@ -2443,11 +2483,9 @@ Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprLocal* local)
Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprGlobal* global)
{
const RefinementKey* key = dfg->getRefinementKey(global);
std::optional<DefId> rvalueDef = dfg->getRValueDefForCompoundAssign(global);
LUAU_ASSERT(key || rvalueDef);
LUAU_ASSERT(key);
// we'll use whichever of the two definitions we have here.
DefId def = key ? key->def : *rvalueDef;
DefId def = key->def;
/* prepopulateGlobalScope() has already added all global functions to the environment by this point, so any
* global that is not already in-scope is definitely an unknown symbol.
@ -3572,6 +3610,8 @@ TypeId ConstraintGenerator::resolveFunctionType(
// how to quantify/instantiate it.
FunctionType ftv{TypeLevel{}, scope.get(), {}, {}, argTypes, returnTypes};
ftv.isCheckedFunction = fn->isCheckedFunction();
if (FFlag::LuauDeprecatedAttribute)
ftv.isDeprecatedFunction = fn->hasAttribute(AstAttr::Type::Deprecated);
// This replicates the behavior of the appropriate FunctionType
// constructors.

View file

@ -38,6 +38,9 @@ LUAU_FASTFLAGVARIABLE(LuauTrackInteriorFreeTablesOnScope)
LUAU_FASTFLAGVARIABLE(LuauPrecalculateMutatedFreeTypes2)
LUAU_FASTFLAGVARIABLE(DebugLuauGreedyGeneralization)
LUAU_FASTFLAG(LuauSearchForRefineableType)
LUAU_FASTFLAG(LuauDeprecatedAttribute)
LUAU_FASTFLAG(LuauBidirectionalInferenceCollectIndexerTypes)
LUAU_FASTFLAG(LuauNewTypeFunReductionChecks2)
namespace Luau
{
@ -625,14 +628,6 @@ bool ConstraintSolver::isDone() const
struct TypeSearcher : TypeVisitor
{
enum struct Polarity: uint8_t
{
None = 0b00,
Positive = 0b01,
Negative = 0b10,
Mixed = 0b11,
};
TypeId needle;
Polarity current = Polarity::Positive;
@ -748,12 +743,12 @@ void ConstraintSolver::generalizeOneType(TypeId ty)
switch (ts.result)
{
case TypeSearcher::Polarity::None:
case Polarity::None:
asMutable(ty)->reassign(Type{BoundType{upperBound}});
break;
case TypeSearcher::Polarity::Negative:
case TypeSearcher::Polarity::Mixed:
case Polarity::Negative:
case Polarity::Mixed:
if (get<UnknownType>(upperBound) && ts.count > 1)
{
asMutable(ty)->reassign(Type{GenericType{tyScope}});
@ -763,15 +758,17 @@ void ConstraintSolver::generalizeOneType(TypeId ty)
asMutable(ty)->reassign(Type{BoundType{upperBound}});
break;
case TypeSearcher::Polarity::Positive:
if (get<UnknownType>(lowerBound) && ts.count > 1)
{
asMutable(ty)->reassign(Type{GenericType{tyScope}});
function->generics.emplace_back(ty);
}
else
asMutable(ty)->reassign(Type{BoundType{lowerBound}});
break;
case Polarity::Positive:
if (get<UnknownType>(lowerBound) && ts.count > 1)
{
asMutable(ty)->reassign(Type{GenericType{tyScope}});
function->generics.emplace_back(ty);
}
else
asMutable(ty)->reassign(Type{BoundType{lowerBound}});
break;
default:
LUAU_ASSERT(!"Unreachable");
}
}
}
@ -918,6 +915,15 @@ bool ConstraintSolver::tryDispatch(const GeneralizationConstraint& c, NotNull<co
bind(constraint, generalizedType, *generalizedTy);
else
unify(constraint, generalizedType, *generalizedTy);
if (FFlag::LuauDeprecatedAttribute)
{
if (FunctionType* fty = getMutable<FunctionType>(follow(generalizedType)))
{
if (c.hasDeprecatedAttribute)
fty->isDeprecatedFunction = true;
}
}
}
else
{
@ -931,12 +937,12 @@ bool ConstraintSolver::tryDispatch(const GeneralizationConstraint& c, NotNull<co
// clang-tidy doesn't understand this is safe.
if (constraint->scope->interiorFreeTypes)
for (TypeId ty : *constraint->scope->interiorFreeTypes) // NOLINT(bugprone-unchecked-optional-access)
generalize(NotNull{arena}, builtinTypes, constraint->scope, generalizedTypes, ty, /* avoidSealingTables */ false);
generalize(NotNull{arena}, builtinTypes, constraint->scope, generalizedTypes, ty);
}
else
{
for (TypeId ty : c.interiorTypes)
generalize(NotNull{arena}, builtinTypes, constraint->scope, generalizedTypes, ty, /* avoidSealingTables */ false);
generalize(NotNull{arena}, builtinTypes, constraint->scope, generalizedTypes, ty);
}
@ -1352,7 +1358,7 @@ void ConstraintSolver::fillInDiscriminantTypes(NotNull<const Constraint> constra
if (isBlocked(*ty))
// We bind any unused discriminants to the `*no-refine*` type indicating that it can be safely ignored.
emplaceType<BoundType>(asMutable(follow(*ty)), builtinTypes->noRefineType);
// We also need to unconditionally unblock these types, otherwise
// you end up with funky looking "Blocked on *no-refine*."
unblock(*ty, constraint->location);
@ -1558,6 +1564,43 @@ static AstExpr* unwrapGroup(AstExpr* expr)
return expr;
}
struct ContainsGenerics : public TypeOnceVisitor
{
DenseHashSet<const void*> generics{nullptr};
bool found = false;
bool visit(TypeId ty) override
{
return !found;
}
bool visit(TypeId ty, const GenericType&) override
{
found |= generics.contains(ty);
return true;
}
bool visit(TypeId ty, const TypeFunctionInstanceType&) override
{
return !found;
}
bool visit(TypePackId tp, const GenericTypePack&) override
{
found |= generics.contains(tp);
return !found;
}
bool hasGeneric(TypeId ty)
{
traverse(ty);
auto ret = found;
found = false;
return ret;
}
};
bool ConstraintSolver::tryDispatch(const FunctionCheckConstraint& c, NotNull<const Constraint> constraint)
{
TypeId fn = follow(c.fn);
@ -1600,36 +1643,49 @@ bool ConstraintSolver::tryDispatch(const FunctionCheckConstraint& c, NotNull<con
DenseHashMap<TypeId, TypeId> replacements{nullptr};
DenseHashMap<TypePackId, TypePackId> replacementPacks{nullptr};
ContainsGenerics containsGenerics;
for (auto generic : ftv->generics)
{
replacements[generic] = builtinTypes->unknownType;
if (FFlag::LuauBidirectionalInferenceCollectIndexerTypes)
containsGenerics.generics.insert(generic);
}
for (auto genericPack : ftv->genericPacks)
{
replacementPacks[genericPack] = builtinTypes->unknownTypePack;
if (FFlag::LuauBidirectionalInferenceCollectIndexerTypes)
containsGenerics.generics.insert(genericPack);
}
// If the type of the function has generics, we don't actually want to push any of the generics themselves
// into the argument types as expected types because this creates an unnecessary loop. Instead, we want to
// replace these types with `unknown` (and `...unknown`) to keep any structure but not create the cycle.
if (!replacements.empty() || !replacementPacks.empty())
if (!FFlag::LuauBidirectionalInferenceCollectIndexerTypes)
{
Replacer replacer{arena, std::move(replacements), std::move(replacementPacks)};
std::optional<TypeId> res = replacer.substitute(fn);
if (res)
if (!replacements.empty() || !replacementPacks.empty())
{
if (*res != fn)
Replacer replacer{arena, std::move(replacements), std::move(replacementPacks)};
std::optional<TypeId> res = replacer.substitute(fn);
if (res)
{
FunctionType* ftvMut = getMutable<FunctionType>(*res);
LUAU_ASSERT(ftvMut);
ftvMut->generics.clear();
ftvMut->genericPacks.clear();
if (*res != fn)
{
FunctionType* ftvMut = getMutable<FunctionType>(*res);
LUAU_ASSERT(ftvMut);
ftvMut->generics.clear();
ftvMut->genericPacks.clear();
}
fn = *res;
ftv = get<FunctionType>(*res);
LUAU_ASSERT(ftv);
// we've potentially copied type functions here, so we need to reproduce their reduce constraint.
reproduceConstraints(constraint->scope, constraint->location, replacer);
}
fn = *res;
ftv = get<FunctionType>(*res);
LUAU_ASSERT(ftv);
// we've potentially copied type functions here, so we need to reproduce their reduce constraint.
reproduceConstraints(constraint->scope, constraint->location, replacer);
}
}
@ -1648,6 +1704,10 @@ bool ConstraintSolver::tryDispatch(const FunctionCheckConstraint& c, NotNull<con
(*c.astExpectedTypes)[expr] = expectedArgTy;
// Generic types are skipped over entirely, for now.
if (FFlag::LuauBidirectionalInferenceCollectIndexerTypes && containsGenerics.hasGeneric(expectedArgTy))
continue;
const FunctionType* expectedLambdaTy = get<FunctionType>(expectedArgTy);
const FunctionType* lambdaTy = get<FunctionType>(actualArgTy);
const AstExprFunction* lambdaExpr = expr->as<AstExprFunction>();
@ -2359,11 +2419,18 @@ bool ConstraintSolver::tryDispatch(const ReduceConstraint& c, NotNull<const Cons
for (TypePackId r : result.reducedPacks)
unblock(r, constraint->location);
if (FFlag::LuauNewTypeFunReductionChecks2)
{
for (TypeId ity : result.irreducibleTypes)
uninhabitedTypeFunctions.insert(ity);
}
bool reductionFinished = result.blockedTypes.empty() && result.blockedPacks.empty();
ty = follow(ty);
// If we couldn't reduce this type function, stick it in the set!
if (get<TypeFunctionInstanceType>(ty))
if (get<TypeFunctionInstanceType>(ty) && (!FFlag::LuauNewTypeFunReductionChecks2 || !result.irreducibleTypes.find(ty)))
typeFunctionsToFinalize[ty] = constraint;
if (force || reductionFinished)
@ -3283,7 +3350,7 @@ void ConstraintSolver::shiftReferences(TypeId source, TypeId target)
}
}
std::optional<TypeId> ConstraintSolver::generalizeFreeType(NotNull<Scope> scope, TypeId type, bool avoidSealingTables)
std::optional<TypeId> ConstraintSolver::generalizeFreeType(NotNull<Scope> scope, TypeId type)
{
TypeId t = follow(type);
if (get<FreeType>(t))
@ -3298,7 +3365,7 @@ std::optional<TypeId> ConstraintSolver::generalizeFreeType(NotNull<Scope> scope,
// that until all constraint generation is complete.
}
return generalize(NotNull{arena}, builtinTypes, scope, generalizedTypes, type, avoidSealingTables);
return generalize(NotNull{arena}, builtinTypes, scope, generalizedTypes, type);
}
bool ConstraintSolver::hasUnresolvedConstraints(TypeId ty)

View file

@ -82,12 +82,6 @@ std::optional<DefId> DataFlowGraph::getDefOptional(const AstExpr* expr) const
return NotNull{*def};
}
std::optional<DefId> DataFlowGraph::getRValueDefForCompoundAssign(const AstExpr* expr) const
{
auto def = compoundAssignDefs.find(expr);
return def ? std::optional<DefId>(*def) : std::nullopt;
}
DefId DataFlowGraph::getDef(const AstLocal* local) const
{
auto def = localDefs.find(local);

View file

@ -1,8 +1,6 @@
// 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_FASTFLAGVARIABLE(LuauDebugInfoDefn)
namespace Luau
{
@ -215,15 +213,6 @@ declare debug: {
)BUILTIN_SRC";
static const std::string kBuiltinDefinitionDebugSrc_DEPRECATED = R"BUILTIN_SRC(
declare debug: {
info: (<R...>(thread: thread, level: number, options: string) -> R...) & (<R...>(level: number, options: string) -> R...) & (<A..., R1..., R2...>(func: (A...) -> R1..., options: string) -> R2...),
traceback: ((message: string?, level: number?) -> string) & ((thread: thread, message: string?, level: number?) -> string),
}
)BUILTIN_SRC";
static const std::string kBuiltinDefinitionUtf8Src = R"BUILTIN_SRC(
declare utf8: {
@ -309,7 +298,7 @@ std::string getBuiltinDefinitionSource()
result += kBuiltinDefinitionOsSrc;
result += kBuiltinDefinitionCoroutineSrc;
result += kBuiltinDefinitionTableSrc;
result += FFlag::LuauDebugInfoDefn ? kBuiltinDefinitionDebugSrc : kBuiltinDefinitionDebugSrc_DEPRECATED;
result += kBuiltinDefinitionDebugSrc;
result += kBuiltinDefinitionUtf8Src;
result += kBuiltinDefinitionBufferSrc;
result += kBuiltinDefinitionVectorSrc;

View file

@ -28,10 +28,9 @@ LUAU_FASTINT(LuauTypeInferIterationLimit);
LUAU_FASTINT(LuauTarjanChildLimit)
LUAU_FASTFLAG(LuauAutocompleteRefactorsForIncrementalAutocomplete)
LUAU_FASTFLAGVARIABLE(LuauIncrementalAutocompleteBugfixes)
LUAU_FASTFLAGVARIABLE(LuauMixedModeDefFinderTraversesTypeOf)
LUAU_FASTFLAGVARIABLE(LuauCloneIncrementalModule)
LUAU_FASTFLAGVARIABLE(LogFragmentsFromAutocomplete)
LUAU_FASTFLAGVARIABLE(DebugLogFragmentsFromAutocomplete)
LUAU_FASTFLAGVARIABLE(LuauBetterCursorInCommentDetection)
LUAU_FASTFLAGVARIABLE(LuauAllFreeTypesHaveScopes)
LUAU_FASTFLAGVARIABLE(LuauFragmentAcSupportsReporter)
@ -42,6 +41,7 @@ LUAU_FASTFLAGVARIABLE(LuauCloneReturnTypePack)
LUAU_FASTFLAGVARIABLE(LuauIncrementalAutocompleteDemandBasedCloning)
LUAU_FASTFLAG(LuauUserTypeFunTypecheck)
LUAU_FASTFLAGVARIABLE(LuauFragmentNoTypeFunEval)
LUAU_FASTFLAGVARIABLE(LuauBetterScopeSelection)
namespace
{
@ -87,6 +87,333 @@ void cloneModuleMap(
}
}
static std::pair<size_t, size_t> getDocumentOffsets(std::string_view src, const Position& startPos, const Position& endPos);
// when typing a function partially, get the span of the first line
// e.g. local function fn() : ... - typically we want to provide autocomplete results if you're
// editing type annotations in this range
Location getFunctionDeclarationExtents(AstExprFunction* exprFn, AstExpr* exprName = nullptr, AstLocal* localName = nullptr)
{
auto fnBegin = exprFn->location.begin;
auto fnEnd = exprFn->location.end;
if (auto returnAnnot = exprFn->returnAnnotation)
{
if (returnAnnot->tailType)
fnEnd = returnAnnot->tailType->location.end;
else if (returnAnnot->types.size != 0)
fnEnd = returnAnnot->types.data[returnAnnot->types.size - 1]->location.end;
}
else if (exprFn->args.size != 0)
{
auto last = exprFn->args.data[exprFn->args.size - 1];
if (last->annotation)
fnEnd = last->annotation->location.end;
else
fnEnd = last->location.end;
}
else if (exprFn->genericPacks.size != 0)
fnEnd = exprFn->genericPacks.data[exprFn->genericPacks.size - 1]->location.end;
else if (exprFn->generics.size != 0)
fnEnd = exprFn->generics.data[exprFn->generics.size - 1]->location.end;
else if (exprName)
fnEnd = exprName->location.end;
else if (localName)
fnEnd = localName->location.end;
return Location{fnBegin, fnEnd};
};
Location getAstStatForExtents(AstStatFor* forStat)
{
auto begin = forStat->location.begin;
auto end = forStat->location.end;
if (forStat->step)
end = forStat->step->location.end;
else if (forStat->to)
end = forStat->to->location.end;
else if (forStat->from)
end = forStat->from->location.end;
else if (forStat->var)
end = forStat->var->location.end;
return Location{begin, end};
}
Location getFragmentLocation(AstStat* nearestStatement, const Position& cursorPosition)
{
Location empty{cursorPosition, cursorPosition};
if (nearestStatement)
{
Location nonEmpty{nearestStatement->location.begin, cursorPosition};
// If your sibling is a do block, do nothing
if (auto doEnd = nearestStatement->as<AstStatBlock>())
return empty;
// If you're inside the body of the function and this is your sibling, empty fragment
// If you're outside the body (e.g. you're typing stuff out, non-empty)
if (auto fn = nearestStatement->as<AstStatFunction>())
{
auto loc = getFunctionDeclarationExtents(fn->func, fn->name, /* local */ nullptr);
if (loc.containsClosed(cursorPosition))
return nonEmpty;
else if (fn->func->body->location.containsClosed(cursorPosition) || fn->location.end <= cursorPosition)
return empty;
else if (fn->func->location.contains(cursorPosition))
return nonEmpty;
}
if (auto fn = nearestStatement->as<AstStatLocalFunction>())
{
auto loc = getFunctionDeclarationExtents(fn->func, /* global func */ nullptr, fn->name);
if (loc.containsClosed(cursorPosition))
return nonEmpty;
else if (fn->func->body->location.containsClosed(cursorPosition) || fn->location.end <= cursorPosition)
return empty;
else if (fn->func->location.contains(cursorPosition))
return nonEmpty;
}
if (auto wh = nearestStatement->as<AstStatWhile>())
{
if (!wh->hasDo)
return nonEmpty;
else
return empty;
}
if (auto forStat = nearestStatement->as<AstStatFor>())
{
if (!forStat->hasDo)
return nonEmpty;
else
return empty;
}
if (auto forIn = nearestStatement->as<AstStatForIn>())
{
// If we don't have a do statement
if (!forIn->hasDo)
return nonEmpty;
else
return empty;
}
if (auto ifS = nearestStatement->as<AstStatIf>())
{
auto conditionExtents = Location{ifS->location.begin, ifS->condition->location.end};
if (conditionExtents.containsClosed(cursorPosition))
return nonEmpty;
else if (ifS->thenbody->location.containsClosed(cursorPosition))
return empty;
else if (auto elseS = ifS->elsebody)
{
if (auto elseIf = ifS->elsebody->as<AstStatIf>())
{
if (elseIf->thenbody->hasEnd)
return empty;
else
return {elseS->location.begin, cursorPosition};
}
return empty;
}
}
return nonEmpty;
}
return empty;
}
struct NearestStatementFinder : public AstVisitor
{
explicit NearestStatementFinder(const Position& cursorPosition)
: cursor(cursorPosition)
{
}
bool visit(AstStatBlock* block) override
{
if (block->location.containsClosed(cursor))
{
parent = block;
for (auto v : block->body)
{
if (v->location.begin <= cursor)
{
nearest = v;
}
}
return true;
}
else
return false;
}
const Position& cursor;
AstStat* nearest = nullptr;
AstStatBlock* parent = nullptr;
};
FragmentRegion getFragmentRegion(AstStatBlock* root, const Position& cursorPosition)
{
NearestStatementFinder nsf{cursorPosition};
root->visit(&nsf);
AstStatBlock* parent = root;
if (nsf.parent)
parent = nsf.parent;
return FragmentRegion{getFragmentLocation(nsf.nearest, cursorPosition), nsf.nearest, parent};
};
FragmentAutocompleteAncestryResult findAncestryForFragmentParse(AstStatBlock* stale, const Position& cursorPos, AstStatBlock* lastGoodParse)
{
// the freshest ast can sometimes be null if the parse was bad.
if (lastGoodParse == nullptr)
return {};
FragmentRegion region = getFragmentRegion(lastGoodParse, cursorPos);
std::vector<AstNode*> ancestry = findAncestryAtPositionForAutocomplete(stale, cursorPos);
LUAU_ASSERT(ancestry.size() >= 1);
// We should only pick up locals that are before the region
DenseHashMap<AstName, AstLocal*> localMap{AstName()};
std::vector<AstLocal*> localStack;
for (AstNode* node : ancestry)
{
if (auto block = node->as<AstStatBlock>())
{
for (auto stat : block->body)
{
if (stat->location.begin < region.fragmentLocation.begin)
{
// This statement precedes the current one
if (auto statLoc = stat->as<AstStatLocal>())
{
for (auto v : statLoc->vars)
{
localStack.push_back(v);
localMap[v->name] = v;
}
}
else if (auto locFun = stat->as<AstStatLocalFunction>())
{
localStack.push_back(locFun->name);
localMap[locFun->name->name] = locFun->name;
if (locFun->location.contains(cursorPos))
{
for (AstLocal* loc : locFun->func->args)
{
localStack.push_back(loc);
localMap[loc->name] = loc;
}
}
}
else if (auto globFun = stat->as<AstStatFunction>())
{
if (globFun->location.contains(cursorPos))
{
for (AstLocal* loc : globFun->func->args)
{
localStack.push_back(loc);
localMap[loc->name] = loc;
}
}
}
else if (auto typeFun = stat->as<AstStatTypeFunction>(); typeFun)
{
if (typeFun->location.contains(cursorPos))
{
for (AstLocal* loc : typeFun->body->args)
{
localStack.push_back(loc);
localMap[loc->name] = loc;
}
}
}
else if (auto forL = stat->as<AstStatFor>())
{
if (forL->var && forL->var->location.begin < region.fragmentLocation.begin)
{
localStack.push_back(forL->var);
localMap[forL->var->name] = forL->var;
}
}
else if (auto forIn = stat->as<AstStatForIn>())
{
for (auto var : forIn->vars)
{
if (var->location.begin < region.fragmentLocation.begin)
{
localStack.push_back(var);
localMap[var->name] = var;
}
}
}
}
}
}
if (auto exprFunc = node->as<AstExprFunction>())
{
if (exprFunc->location.contains(cursorPos))
{
for (auto v : exprFunc->args)
{
localStack.push_back(v);
localMap[v->name] = v;
}
}
}
}
return {localMap, localStack, ancestry, region.nearestStatement, region.parentBlock, region.fragmentLocation};
}
std::optional<FragmentParseResult> parseFragment(
AstStatBlock* stale,
AstStatBlock* mostRecentParse,
AstNameTable* names,
std::string_view src,
const Position& cursorPos,
std::optional<Position> fragmentEndPosition
)
{
if (mostRecentParse == nullptr)
return std::nullopt;
FragmentAutocompleteAncestryResult result = findAncestryForFragmentParse(stale, cursorPos, mostRecentParse);
AstStat* nearestStatement = result.nearestStatement;
Position startPos = result.fragmentSelectionRegion.begin;
Position endPos = fragmentEndPosition.value_or(result.fragmentSelectionRegion.end);
auto [offsetStart, parseLength] = getDocumentOffsets(src, startPos, endPos);
const char* srcStart = src.data() + offsetStart;
std::string_view dbg = src.substr(offsetStart, parseLength);
FragmentParseResult fragmentResult;
fragmentResult.fragmentToParse = std::string(dbg);
// For the duration of the incremental parse, we want to allow the name table to re-use duplicate names
if (FFlag::DebugLogFragmentsFromAutocomplete)
logLuau("Fragment Selected", dbg);
ParseOptions opts;
opts.allowDeclarationSyntax = false;
opts.captureComments = true;
opts.parseFragment = FragmentParseResumeSettings{std::move(result.localMap), std::move(result.localStack), startPos};
ParseResult p = Luau::Parser::parse(srcStart, parseLength, *names, *fragmentResult.alloc, opts);
// This means we threw a ParseError and we should decline to offer autocomplete here.
if (p.root == nullptr)
return std::nullopt;
std::vector<AstNode*> fabricatedAncestry = std::move(result.ancestry);
std::vector<AstNode*> fragmentAncestry = findAncestryAtPositionForAutocomplete(p.root, cursorPos);
fabricatedAncestry.insert(fabricatedAncestry.end(), fragmentAncestry.begin(), fragmentAncestry.end());
if (nearestStatement == nullptr)
nearestStatement = p.root;
fragmentResult.root = p.root;
fragmentResult.ancestry = std::move(fabricatedAncestry);
fragmentResult.nearestStatement = nearestStatement;
fragmentResult.commentLocations = std::move(p.commentLocations);
fragmentResult.scopePos = result.parentBlock->location.begin;
return fragmentResult;
}
struct UsageFinder : public AstVisitor
{
@ -158,6 +485,7 @@ void cloneTypesFromFragment(
const ModulePtr& staleModule,
NotNull<TypeArena> destArena,
NotNull<DataFlowGraph> dfg,
NotNull<BuiltinTypes> builtins,
AstStatBlock* program,
Scope* destScope
)
@ -188,6 +516,13 @@ void cloneTypesFromFragment(
destScope->lvalueTypes[d] = Luau::cloneIncremental(pair->second.typeId, *destArena, cloneState, destScope);
destScope->bindings[pair->first] = Luau::cloneIncremental(pair->second, *destArena, cloneState, destScope);
}
else if (FFlag::LuauBetterScopeSelection)
{
destScope->lvalueTypes[d] = builtins->unknownType;
Binding b;
b.typeId = builtins->unknownType;
destScope->bindings[Symbol(loc)] = b;
}
}
// Second - any referenced type alias bindings need to be placed in scope so type annotation can be resolved.
@ -400,15 +735,10 @@ static FrontendModuleResolver& getModuleResolver(Frontend& frontend, std::option
bool statIsBeforePos(const AstNode* stat, const Position& cursorPos)
{
if (FFlag::LuauIncrementalAutocompleteBugfixes)
{
return (stat->location.begin < cursorPos);
}
return stat->location.begin < cursorPos && stat->location.begin.line < cursorPos.line;
return (stat->location.begin < cursorPos);
}
FragmentAutocompleteAncestryResult findAncestryForFragmentParse(AstStatBlock* root, const Position& cursorPos)
FragmentAutocompleteAncestryResult findAncestryForFragmentParse_DEPRECATED(AstStatBlock* root, const Position& cursorPos)
{
std::vector<AstNode*> ancestry = findAncestryAtPositionForAutocomplete(root, cursorPos);
// Should always contain the root AstStat
@ -437,7 +767,7 @@ FragmentAutocompleteAncestryResult findAncestryForFragmentParse(AstStatBlock* ro
{
for (auto stat : block->body)
{
if (statIsBeforePos(stat, FFlag::LuauIncrementalAutocompleteBugfixes ? nearestStatement->location.begin : cursorPos))
if (statIsBeforePos(stat, nearestStatement->location.begin))
{
// This statement precedes the current one
if (auto statLoc = stat->as<AstStatLocal>())
@ -486,17 +816,14 @@ FragmentAutocompleteAncestryResult findAncestryForFragmentParse(AstStatBlock* ro
}
}
}
if (FFlag::LuauIncrementalAutocompleteBugfixes)
if (auto exprFunc = node->as<AstExprFunction>())
{
if (auto exprFunc = node->as<AstExprFunction>())
if (exprFunc->location.contains(cursorPos))
{
if (exprFunc->location.contains(cursorPos))
for (auto v : exprFunc->args)
{
for (auto v : exprFunc->args)
{
localStack.push_back(v);
localMap[v->name] = v;
}
localStack.push_back(v);
localMap[v->name] = v;
}
}
}
@ -513,7 +840,7 @@ FragmentAutocompleteAncestryResult findAncestryForFragmentParse(AstStatBlock* ro
* Example - your document is "foo bar baz" and getDocumentOffsets is passed (0, 4), (0, 8). This function returns the pair {3, 5}
* which corresponds to the string " bar "
*/
std::pair<size_t, size_t> getDocumentOffsets(const std::string_view& src, const Position& startPos, const Position& endPos)
static std::pair<size_t, size_t> getDocumentOffsets(std::string_view src, const Position& startPos, const Position& endPos)
{
size_t lineCount = 0;
size_t colCount = 0;
@ -570,14 +897,14 @@ std::pair<size_t, size_t> getDocumentOffsets(const std::string_view& src, const
return {min, len};
}
ScopePtr findClosestScope(const ModulePtr& module, const AstStat* nearestStatement)
ScopePtr findClosestScope_DEPRECATED(const ModulePtr& module, const AstStat* nearestStatement)
{
LUAU_ASSERT(module->hasModuleScope());
ScopePtr closest = module->getModuleScope();
// find the scope the nearest statement belonged to.
for (auto [loc, sc] : module->scopes)
for (const auto& [loc, sc] : module->scopes)
{
if (loc.encloses(nearestStatement->location) && closest->location.begin <= loc.begin)
closest = sc;
@ -586,7 +913,23 @@ ScopePtr findClosestScope(const ModulePtr& module, const AstStat* nearestStateme
return closest;
}
std::optional<FragmentParseResult> parseFragment(
ScopePtr findClosestScope(const ModulePtr& module, const Position& scopePos)
{
LUAU_ASSERT(module->hasModuleScope());
ScopePtr closest = module->getModuleScope();
// find the scope the nearest statement belonged to.
for (const auto& [loc, sc] : module->scopes)
{
if (sc->location.contains(scopePos) && closest->location.begin < sc->location.begin)
closest = sc;
}
return closest;
}
std::optional<FragmentParseResult> parseFragment_DEPRECATED(
AstStatBlock* root,
AstNameTable* names,
std::string_view src,
@ -594,7 +937,7 @@ std::optional<FragmentParseResult> parseFragment(
std::optional<Position> fragmentEndPosition
)
{
FragmentAutocompleteAncestryResult result = findAncestryForFragmentParse(root, cursorPos);
FragmentAutocompleteAncestryResult result = findAncestryForFragmentParse_DEPRECATED(root, cursorPos);
AstStat* nearestStatement = result.nearestStatement;
const Location& rootSpan = root->location;
@ -625,8 +968,8 @@ std::optional<FragmentParseResult> parseFragment(
FragmentParseResult fragmentResult;
fragmentResult.fragmentToParse = std::string(dbg.data(), parseLength);
// For the duration of the incremental parse, we want to allow the name table to re-use duplicate names
if (FFlag::LogFragmentsFromAutocomplete)
logLuau(dbg);
if (FFlag::DebugLogFragmentsFromAutocomplete)
logLuau("Fragment Selected", dbg);
ParseOptions opts;
opts.allowDeclarationSyntax = false;
@ -1025,7 +1368,14 @@ FragmentTypeCheckResult typecheckFragment_(
reportWaypoint(reporter, FragmentAutocompleteWaypoint::CloneAndSquashScopeStart);
cloneTypesFromFragment(
cloneState, closestScope.get(), stale, NotNull{&incrementalModule->internalTypes}, NotNull{&dfg}, root, freshChildOfNearestScope.get()
cloneState,
closestScope.get(),
stale,
NotNull{&incrementalModule->internalTypes},
NotNull{&dfg},
frontend.builtinTypes,
root,
freshChildOfNearestScope.get()
);
reportWaypoint(reporter, FragmentAutocompleteWaypoint::CloneAndSquashScopeEnd);
@ -1086,6 +1436,7 @@ std::pair<FragmentTypeCheckStatus, FragmentTypeCheckResult> typecheckFragment(
std::optional<FrontendOptions> opts,
std::string_view src,
std::optional<Position> fragmentEndPosition,
AstStatBlock* recentParse,
IFragmentAutocompleteReporter* reporter
)
{
@ -1106,7 +1457,9 @@ std::pair<FragmentTypeCheckStatus, FragmentTypeCheckResult> typecheckFragment(
std::optional<FragmentParseResult> tryParse;
if (FFlag::LuauModuleHoldsAstRoot)
{
tryParse = parseFragment(module->root, module->names.get(), src, cursorPos, fragmentEndPosition);
tryParse = FFlag::LuauBetterScopeSelection
? parseFragment(module->root, recentParse, module->names.get(), src, cursorPos, fragmentEndPosition)
: parseFragment_DEPRECATED(module->root, module->names.get(), src, cursorPos, fragmentEndPosition);
}
else
{
@ -1117,15 +1470,12 @@ std::pair<FragmentTypeCheckStatus, FragmentTypeCheckResult> typecheckFragment(
return {};
}
if (FFlag::LuauIncrementalAutocompleteBugfixes)
if (sourceModule->allocator.get() != module->allocator.get())
{
if (sourceModule->allocator.get() != module->allocator.get())
{
return {FragmentTypeCheckStatus::SkipAutocomplete, {}};
}
return {FragmentTypeCheckStatus::SkipAutocomplete, {}};
}
tryParse = parseFragment(sourceModule->root, sourceModule->names.get(), src, cursorPos, fragmentEndPosition);
tryParse = parseFragment_DEPRECATED(sourceModule->root, sourceModule->names.get(), src, cursorPos, fragmentEndPosition);
reportWaypoint(reporter, FragmentAutocompleteWaypoint::ParseFragmentEnd);
}
@ -1138,7 +1488,8 @@ std::pair<FragmentTypeCheckStatus, FragmentTypeCheckResult> typecheckFragment(
return {FragmentTypeCheckStatus::SkipAutocomplete, {}};
FrontendOptions frontendOptions = opts.value_or(frontend.options);
const ScopePtr& closestScope = findClosestScope(module, parseResult.nearestStatement);
const ScopePtr& closestScope = FFlag::LuauBetterScopeSelection ? findClosestScope(module, parseResult.scopePos)
: findClosestScope_DEPRECATED(module, parseResult.nearestStatement);
FragmentTypeCheckResult result =
FFlag::LuauIncrementalAutocompleteDemandBasedCloning
? typecheckFragment_(frontend, parseResult.root, module, closestScope, cursorPos, std::move(parseResult.alloc), frontendOptions, reporter)
@ -1174,14 +1525,15 @@ FragmentAutocompleteStatusResult tryFragmentAutocomplete(
context.opts,
std::move(stringCompletionCB),
context.DEPRECATED_fragmentEndPosition,
context.freshParse.root,
FFlag::LuauFragmentAcSupportsReporter ? context.reporter : nullptr
);
return {FragmentAutocompleteStatus::Success, std::move(fragmentAutocomplete)};
}
catch (const Luau::InternalCompilerError& e)
{
if (FFlag::LogFragmentsFromAutocomplete)
logLuau(e.what());
if (FFlag::DebugLogFragmentsFromAutocomplete)
logLuau("tryFragmentAutocomplete exception", e.what());
return {FragmentAutocompleteStatus::InternalIce, std::nullopt};
}
}
@ -1194,6 +1546,7 @@ FragmentAutocompleteResult fragmentAutocomplete(
std::optional<FrontendOptions> opts,
StringCompletionCallback callback,
std::optional<Position> fragmentEndPosition,
AstStatBlock* recentParse,
IFragmentAutocompleteReporter* reporter
)
{
@ -1215,14 +1568,14 @@ FragmentAutocompleteResult fragmentAutocomplete(
return {};
}
auto [tcStatus, tcResult] = typecheckFragment(frontend, moduleName, cursorPosition, opts, src, fragmentEndPosition, reporter);
auto [tcStatus, tcResult] = typecheckFragment(frontend, moduleName, cursorPosition, opts, src, fragmentEndPosition, recentParse, reporter);
if (tcStatus == FragmentTypeCheckStatus::SkipAutocomplete)
return {};
reportWaypoint(reporter, FragmentAutocompleteWaypoint::TypecheckFragmentEnd);
auto globalScope = (opts && opts->forAutocomplete) ? frontend.globalsForAutocomplete.globalScope.get() : frontend.globals.globalScope.get();
if (FFlag::LogFragmentsFromAutocomplete)
logLuau(src);
if (FFlag::DebugLogFragmentsFromAutocomplete)
logLuau("Fragment Autocomplete Source Script", src);
TypeArena arenaForFragmentAutocomplete;
auto result = Luau::autocomplete_(
tcResult.incrementalModule,

View file

@ -1,7 +1,6 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#include "Luau/Frontend.h"
#include "Luau/AnyTypeSummary.h"
#include "Luau/BuiltinDefinitions.h"
#include "Luau/Clone.h"
#include "Luau/Common.h"
@ -51,9 +50,8 @@ LUAU_FASTFLAGVARIABLE(LuauModuleHoldsAstRoot)
LUAU_FASTFLAGVARIABLE(LuauFixMultithreadTypecheck)
LUAU_FASTFLAG(StudioReportLuauAny2)
LUAU_FASTFLAGVARIABLE(LuauSelectivelyRetainDFGArena)
LUAU_FASTFLAG(LuauTypeFunResultInAutocomplete)
namespace Luau
{
@ -461,20 +459,6 @@ CheckResult Frontend::check(const ModuleName& name, std::optional<FrontendOption
if (item.name == name)
checkResult.lintResult = item.module->lintResult;
if (FFlag::StudioReportLuauAny2 && item.options.retainFullTypeGraphs)
{
if (item.module)
{
const SourceModule& sourceModule = *item.sourceModule;
if (sourceModule.mode == Luau::Mode::Strict)
{
item.module->ats.root = toString(sourceModule.root);
}
item.module->ats.rootSrc = sourceModule.root;
item.module->ats.traverse(item.module.get(), sourceModule.root, NotNull{&builtinTypes_});
}
}
}
return checkResult;
@ -1658,7 +1642,7 @@ ModulePtr check(
SimplifierPtr simplifier = newSimplifier(NotNull{&result->internalTypes}, builtinTypes);
TypeFunctionRuntime typeFunctionRuntime{iceHandler, NotNull{&limits}};
typeFunctionRuntime.allowEvaluation = sourceModule.parseErrors.empty();
typeFunctionRuntime.allowEvaluation = FFlag::LuauTypeFunResultInAutocomplete || sourceModule.parseErrors.empty();
ConstraintGenerator cg{
result,

View file

@ -30,7 +30,6 @@ struct MutatingGeneralizer : TypeOnceVisitor
std::vector<TypePackId> genericPacks;
bool isWithinFunction = false;
bool avoidSealingTables = false;
MutatingGeneralizer(
NotNull<TypeArena> arena,
@ -38,8 +37,7 @@ struct MutatingGeneralizer : TypeOnceVisitor
NotNull<Scope> scope,
NotNull<DenseHashSet<TypeId>> cachedTypes,
DenseHashMap<const void*, size_t> positiveTypes,
DenseHashMap<const void*, size_t> negativeTypes,
bool avoidSealingTables
DenseHashMap<const void*, size_t> negativeTypes
)
: TypeOnceVisitor(/* skipBoundTypes */ true)
, arena(arena)
@ -48,7 +46,6 @@ struct MutatingGeneralizer : TypeOnceVisitor
, cachedTypes(cachedTypes)
, positiveTypes(std::move(positiveTypes))
, negativeTypes(std::move(negativeTypes))
, avoidSealingTables(avoidSealingTables)
{
}
@ -145,7 +142,7 @@ struct MutatingGeneralizer : TypeOnceVisitor
TypeId onlyType = it->parts[0];
LUAU_ASSERT(onlyType != needle);
emplaceType<BoundType>(asMutable(needle), onlyType);
}
}
else if (FFlag::LuauGeneralizationRemoveRecursiveUpperBound2 && it->parts.empty())
{
emplaceType<BoundType>(asMutable(needle), builtinTypes->unknownType);
@ -292,8 +289,7 @@ struct MutatingGeneralizer : TypeOnceVisitor
TableType* tt = getMutable<TableType>(ty);
LUAU_ASSERT(tt);
if (!avoidSealingTables)
tt->state = TableState::Sealed;
tt->state = TableState::Sealed;
return true;
}
@ -332,26 +328,19 @@ struct FreeTypeSearcher : TypeVisitor
{
}
enum Polarity
{
Positive,
Negative,
Both,
};
Polarity polarity = Positive;
Polarity polarity = Polarity::Positive;
void flip()
{
switch (polarity)
{
case Positive:
polarity = Negative;
case Polarity::Positive:
polarity = Polarity::Negative;
break;
case Negative:
polarity = Positive;
case Polarity::Negative:
polarity = Polarity::Positive;
break;
case Both:
default:
break;
}
}
@ -363,7 +352,7 @@ struct FreeTypeSearcher : TypeVisitor
{
switch (polarity)
{
case Positive:
case Polarity::Positive:
{
if (seenPositive.contains(ty))
return true;
@ -371,7 +360,7 @@ struct FreeTypeSearcher : TypeVisitor
seenPositive.insert(ty);
return false;
}
case Negative:
case Polarity::Negative:
{
if (seenNegative.contains(ty))
return true;
@ -379,7 +368,7 @@ struct FreeTypeSearcher : TypeVisitor
seenNegative.insert(ty);
return false;
}
case Both:
case Polarity::Mixed:
{
if (seenPositive.contains(ty) && seenNegative.contains(ty))
return true;
@ -388,6 +377,8 @@ struct FreeTypeSearcher : TypeVisitor
seenNegative.insert(ty);
return false;
}
default:
LUAU_ASSERT(!"Unreachable");
}
return false;
@ -418,16 +409,18 @@ struct FreeTypeSearcher : TypeVisitor
switch (polarity)
{
case Positive:
case Polarity::Positive:
positiveTypes[ty]++;
break;
case Negative:
case Polarity::Negative:
negativeTypes[ty]++;
break;
case Both:
case Polarity::Mixed:
positiveTypes[ty]++;
negativeTypes[ty]++;
break;
default:
LUAU_ASSERT(!"Unreachable");
}
return true;
@ -442,16 +435,18 @@ struct FreeTypeSearcher : TypeVisitor
{
switch (polarity)
{
case Positive:
case Polarity::Positive:
positiveTypes[ty]++;
break;
case Negative:
case Polarity::Negative:
negativeTypes[ty]++;
break;
case Both:
case Polarity::Mixed:
positiveTypes[ty]++;
negativeTypes[ty]++;
break;
default:
LUAU_ASSERT(!"Unreachable");
}
}
@ -464,7 +459,7 @@ struct FreeTypeSearcher : TypeVisitor
LUAU_ASSERT(prop.isShared() || FFlag::LuauAutocompleteRefactorsForIncrementalAutocomplete);
Polarity p = polarity;
polarity = Both;
polarity = Polarity::Mixed;
traverse(prop.type());
polarity = p;
}
@ -508,16 +503,18 @@ struct FreeTypeSearcher : TypeVisitor
switch (polarity)
{
case Positive:
case Polarity::Positive:
positiveTypes[tp]++;
break;
case Negative:
case Polarity::Negative:
negativeTypes[tp]++;
break;
case Both:
case Polarity::Mixed:
positiveTypes[tp]++;
negativeTypes[tp]++;
break;
default:
LUAU_ASSERT(!"Unreachable");
}
return true;
@ -972,8 +969,7 @@ std::optional<TypeId> generalize(
NotNull<BuiltinTypes> builtinTypes,
NotNull<Scope> scope,
NotNull<DenseHashSet<TypeId>> cachedTypes,
TypeId ty,
bool avoidSealingTables
TypeId ty
)
{
ty = follow(ty);
@ -984,7 +980,7 @@ std::optional<TypeId> generalize(
FreeTypeSearcher fts{scope, cachedTypes};
fts.traverse(ty);
MutatingGeneralizer gen{arena, builtinTypes, scope, cachedTypes, std::move(fts.positiveTypes), std::move(fts.negativeTypes), avoidSealingTables};
MutatingGeneralizer gen{arena, builtinTypes, scope, cachedTypes, std::move(fts.positiveTypes), std::move(fts.negativeTypes)};
gen.traverse(ty);

View file

@ -19,6 +19,8 @@ LUAU_FASTFLAG(LuauSolverV2)
LUAU_FASTFLAG(LuauAttribute)
LUAU_FASTFLAGVARIABLE(LintRedundantNativeAttribute)
LUAU_FASTFLAG(LuauDeprecatedAttribute)
namespace Luau
{
@ -2280,6 +2282,57 @@ private:
{
}
bool visit(AstExprLocal* node) override
{
if (FFlag::LuauDeprecatedAttribute)
{
const FunctionType* fty = getFunctionType(node);
bool shouldReport = fty && fty->isDeprecatedFunction && !inScope(fty);
if (shouldReport)
report(node->location, node->local->name.value);
}
return true;
}
bool visit(AstExprGlobal* node) override
{
if (FFlag::LuauDeprecatedAttribute)
{
const FunctionType* fty = getFunctionType(node);
bool shouldReport = fty && fty->isDeprecatedFunction && !inScope(fty);
if (shouldReport)
report(node->location, node->name.value);
}
return true;
}
bool visit(AstStatLocalFunction* node) override
{
if (FFlag::LuauDeprecatedAttribute)
{
check(node->func);
return false;
}
else
return true;
}
bool visit(AstStatFunction* node) override
{
if (FFlag::LuauDeprecatedAttribute)
{
check(node->func);
return false;
}
else
return true;
}
bool visit(AstExprIndexName* node) override
{
if (std::optional<TypeId> ty = context->getType(node->expr))
@ -2325,18 +2378,59 @@ private:
if (prop && prop->deprecated)
report(node->location, *prop, cty->name.c_str(), node->index.value);
else if (FFlag::LuauDeprecatedAttribute && prop)
{
if (std::optional<TypeId> ty = prop->readTy)
{
const FunctionType* fty = get<FunctionType>(follow(ty));
bool shouldReport = fty && fty->isDeprecatedFunction && !inScope(fty);
if (shouldReport)
{
const char* className = nullptr;
if (AstExprGlobal* global = node->expr->as<AstExprGlobal>())
className = global->name.value;
const char* functionName = node->index.value;
report(node->location, className, functionName);
}
}
}
}
else if (const TableType* tty = get<TableType>(ty))
{
auto prop = tty->props.find(node->index.value);
if (prop != tty->props.end() && prop->second.deprecated)
if (prop != tty->props.end())
{
// strip synthetic typeof() for builtin tables
if (tty->name && tty->name->compare(0, 7, "typeof(") == 0 && tty->name->back() == ')')
report(node->location, prop->second, tty->name->substr(7, tty->name->length() - 8).c_str(), node->index.value);
else
report(node->location, prop->second, tty->name ? tty->name->c_str() : nullptr, node->index.value);
if (prop->second.deprecated)
{
// strip synthetic typeof() for builtin tables
if (tty->name && tty->name->compare(0, 7, "typeof(") == 0 && tty->name->back() == ')')
report(node->location, prop->second, tty->name->substr(7, tty->name->length() - 8).c_str(), node->index.value);
else
report(node->location, prop->second, tty->name ? tty->name->c_str() : nullptr, node->index.value);
}
else if (FFlag::LuauDeprecatedAttribute)
{
if (std::optional<TypeId> ty = prop->second.readTy)
{
const FunctionType* fty = get<FunctionType>(follow(ty));
bool shouldReport = fty && fty->isDeprecatedFunction && !inScope(fty);
if (shouldReport)
{
const char* className = nullptr;
if (AstExprGlobal* global = node->expr->as<AstExprGlobal>())
className = global->name.value;
const char* functionName = node->index.value;
report(node->location, className, functionName);
}
}
}
}
}
}
@ -2355,6 +2449,26 @@ private:
}
}
void check(AstExprFunction* func)
{
LUAU_ASSERT(FFlag::LuauDeprecatedAttribute);
LUAU_ASSERT(func);
const FunctionType* fty = getFunctionType(func);
bool isDeprecated = fty && fty->isDeprecatedFunction;
// If a function is deprecated, we don't want to flag its recursive uses.
// So we push it on a stack while its body is being analyzed.
// When a deprecated function is used, we check the stack to ensure that we are not inside that function.
if (isDeprecated)
pushScope(fty);
func->visit(this);
if (isDeprecated)
popScope(fty);
}
void report(const Location& location, const Property& prop, const char* container, const char* field)
{
std::string suggestion = prop.deprecatedSuggestion.empty() ? "" : format(", use '%s' instead", prop.deprecatedSuggestion.c_str());
@ -2364,6 +2478,63 @@ private:
else
emitWarning(*context, LintWarning::Code_DeprecatedApi, location, "Member '%s' is deprecated%s", field, suggestion.c_str());
}
void report(const Location& location, const char* tableName, const char* functionName)
{
LUAU_ASSERT(FFlag::LuauDeprecatedAttribute);
if (tableName)
emitWarning(*context, LintWarning::Code_DeprecatedApi, location, "Member '%s.%s' is deprecated", tableName, functionName);
else
emitWarning(*context, LintWarning::Code_DeprecatedApi, location, "Member '%s' is deprecated", functionName);
}
void report(const Location& location, const char* functionName)
{
LUAU_ASSERT(FFlag::LuauDeprecatedAttribute);
emitWarning(*context, LintWarning::Code_DeprecatedApi, location, "Function '%s' is deprecated", functionName);
}
std::vector<const FunctionType*> functionTypeScopeStack;
void pushScope(const FunctionType* fty)
{
LUAU_ASSERT(FFlag::LuauDeprecatedAttribute);
LUAU_ASSERT(fty);
functionTypeScopeStack.push_back(fty);
}
void popScope(const FunctionType* fty)
{
LUAU_ASSERT(FFlag::LuauDeprecatedAttribute);
LUAU_ASSERT(fty);
LUAU_ASSERT(fty == functionTypeScopeStack.back());
functionTypeScopeStack.pop_back();
}
bool inScope(const FunctionType* fty) const
{
LUAU_ASSERT(FFlag::LuauDeprecatedAttribute);
LUAU_ASSERT(fty);
return std::find(functionTypeScopeStack.begin(), functionTypeScopeStack.end(), fty) != functionTypeScopeStack.end();
}
const FunctionType* getFunctionType(AstExpr* node)
{
LUAU_ASSERT(FFlag::LuauDeprecatedAttribute);
std::optional<TypeId> ty = context->getType(node);
if (!ty)
return nullptr;
const FunctionType* fty = get<FunctionType>(follow(ty));
return fty;
}
};
class LintTableOperations : AstVisitor

View file

@ -20,7 +20,7 @@ LUAU_FASTFLAGVARIABLE(LuauIncrementalAutocompleteCommentDetection)
namespace Luau
{
static void defaultLogLuau(std::string_view input)
static void defaultLogLuau(std::string_view context, std::string_view input)
{
// The default is to do nothing because we don't want to mess with
// the xml parsing done by the dcr script.

View file

@ -2,6 +2,7 @@
#include "Luau/NonStrictTypeChecker.h"
#include "Luau/Ast.h"
#include "Luau/AstQuery.h"
#include "Luau/Common.h"
#include "Luau/Simplify.h"
#include "Luau/Type.h"

View file

@ -17,12 +17,15 @@
LUAU_FASTFLAGVARIABLE(DebugLuauCheckNormalizeInvariant)
LUAU_FASTFLAGVARIABLE(LuauNormalizeNegatedErrorToAnError)
LUAU_FASTFLAGVARIABLE(LuauNormalizeIntersectErrorToAnError)
LUAU_FASTINTVARIABLE(LuauNormalizeCacheLimit, 100000)
LUAU_FASTINTVARIABLE(LuauNormalizeIntersectionLimit, 200)
LUAU_FASTINTVARIABLE(LuauNormalizeUnionLimit, 100)
LUAU_FASTFLAG(LuauSolverV2)
LUAU_FASTFLAGVARIABLE(LuauNormalizeNegationFix)
LUAU_FASTFLAGVARIABLE(LuauFixInfiniteRecursionInNormalization)
LUAU_FASTFLAGVARIABLE(LuauNormalizedBufferIsNotUnknown)
LUAU_FASTFLAGVARIABLE(LuauNormalizeLimitFunctionSet)
namespace Luau
{
@ -581,7 +584,7 @@ NormalizationResult Normalizer::isIntersectionInhabited(TypeId left, TypeId righ
{
left = follow(left);
right = follow(right);
// We're asking if intersection is inahbited between left and right but we've already seen them ....
// We're asking if intersection is inhabited between left and right but we've already seen them ....
if (cacheInhabitance)
{
@ -1687,6 +1690,13 @@ NormalizationResult Normalizer::unionNormals(NormalizedType& here, const Normali
return res;
}
if (FFlag::LuauNormalizeLimitFunctionSet)
{
// Limit based on worst-case expansion of the function unions
if (here.functions.parts.size() * there.functions.parts.size() >= size_t(FInt::LuauNormalizeUnionLimit))
return NormalizationResult::HitLimits;
}
here.booleans = unionOfBools(here.booleans, there.booleans);
unionClasses(here.classes, there.classes);
@ -1698,6 +1708,7 @@ NormalizationResult Normalizer::unionNormals(NormalizedType& here, const Normali
here.buffers = (get<NeverType>(there.buffers) ? here.buffers : there.buffers);
unionFunctions(here.functions, there.functions);
unionTables(here.tables, there.tables);
return NormalizationResult::True;
}
@ -1737,7 +1748,7 @@ NormalizationResult Normalizer::intersectNormalWithNegationTy(TypeId toNegate, N
return NormalizationResult::True;
}
// See above for an explaination of `ignoreSmallerTyvars`.
// See above for an explanation of `ignoreSmallerTyvars`.
NormalizationResult Normalizer::unionNormalWithTy(
NormalizedType& here,
TypeId there,
@ -3052,7 +3063,7 @@ NormalizationResult Normalizer::intersectTyvarsWithTy(
return NormalizationResult::True;
}
// See above for an explaination of `ignoreSmallerTyvars`.
// See above for an explanation of `ignoreSmallerTyvars`.
NormalizationResult Normalizer::intersectNormals(NormalizedType& here, const NormalizedType& there, int ignoreSmallerTyvars)
{
RecursionCounter _rc(&sharedState->counters.recursionCount);
@ -3070,11 +3081,17 @@ NormalizationResult Normalizer::intersectNormals(NormalizedType& here, const Nor
return unionNormals(here, there, ignoreSmallerTyvars);
}
// Limit based on worst-case expansion of the table intersection
// Limit based on worst-case expansion of the table/function intersections
// This restriction can be relaxed when table intersection simplification is improved
if (here.tables.size() * there.tables.size() >= size_t(FInt::LuauNormalizeIntersectionLimit))
return NormalizationResult::HitLimits;
if (FFlag::LuauNormalizeLimitFunctionSet)
{
if (here.functions.parts.size() * there.functions.parts.size() >= size_t(FInt::LuauNormalizeIntersectionLimit))
return NormalizationResult::HitLimits;
}
here.booleans = intersectionOfBools(here.booleans, there.booleans);
intersectClasses(here.classes, there.classes);
@ -3210,7 +3227,7 @@ NormalizationResult Normalizer::intersectNormalWithTy(
{
TypeId errors = here.errors;
clearNormal(here);
here.errors = errors;
here.errors = FFlag::LuauNormalizeIntersectErrorToAnError && get<ErrorType>(errors) ? errors : there;
}
else if (const PrimitiveType* ptv = get<PrimitiveType>(there))
{
@ -3307,11 +3324,16 @@ NormalizationResult Normalizer::intersectNormalWithTy(
clearNormal(here);
return NormalizationResult::True;
}
else if (FFlag::LuauNormalizeNegatedErrorToAnError && get<ErrorType>(t))
{
// ~error is still an error, so intersecting with the negation is the same as intersecting with a type
TypeId errors = here.errors;
clearNormal(here);
here.errors = FFlag::LuauNormalizeIntersectErrorToAnError && get<ErrorType>(errors) ? errors : t;
}
else if (auto nt = get<NegationType>(t))
{
if (FFlag::LuauNormalizeNegationFix)
here.tyvars = std::move(tyvars);
here.tyvars = std::move(tyvars);
return intersectNormalWithTy(here, nt->ty, seenTablePropPairs, seenSetTypes);
}
else

View file

@ -13,6 +13,7 @@ LUAU_FASTINTVARIABLE(LuauTarjanChildLimit, 10000)
LUAU_FASTFLAG(LuauSolverV2)
LUAU_FASTINTVARIABLE(LuauTarjanPreallocationSize, 256)
LUAU_FASTFLAG(LuauSyntheticErrors)
LUAU_FASTFLAG(LuauDeprecatedAttribute)
namespace Luau
{
@ -102,6 +103,8 @@ static TypeId shallowClone(TypeId ty, TypeArena& dest, const TxnLog* log, bool a
clone.tags = a.tags;
clone.argNames = a.argNames;
clone.isCheckedFunction = a.isCheckedFunction;
if (FFlag::LuauDeprecatedAttribute)
clone.isDeprecatedFunction = a.isDeprecatedFunction;
return dest.addType(std::move(clone));
}
else if constexpr (std::is_same_v<T, TableType>)

View file

@ -22,6 +22,7 @@
#include <algorithm>
LUAU_FASTFLAGVARIABLE(DebugLuauSubtypingCheckPathValidity)
LUAU_FASTFLAGVARIABLE(LuauSubtypingStopAtNormFail)
namespace Luau
{
@ -415,6 +416,14 @@ SubtypingResult Subtyping::isSubtype(TypeId subTy, TypeId superTy, NotNull<Scope
SubtypingResult result = isCovariantWith(env, subTy, superTy, scope);
if (FFlag::LuauSubtypingStopAtNormFail && result.normalizationTooComplex)
{
if (result.isCacheable)
resultCache[{subTy, superTy}] = result;
return result;
}
for (const auto& [subTy, bounds] : env.mappedGenerics)
{
const auto& lb = bounds.lowerBound;
@ -592,7 +601,12 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypeId sub
if (!result.isSubtype && !result.normalizationTooComplex)
{
SubtypingResult semantic = isCovariantWith(env, normalizer->normalize(subTy), normalizer->normalize(superTy), scope);
if (semantic.isSubtype)
if (FFlag::LuauSubtypingStopAtNormFail && semantic.normalizationTooComplex)
{
result = semantic;
}
else if (semantic.isSubtype)
{
semantic.reasoning.clear();
result = semantic;
@ -607,7 +621,12 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypeId sub
if (!result.isSubtype && !result.normalizationTooComplex)
{
SubtypingResult semantic = isCovariantWith(env, normalizer->normalize(subTy), normalizer->normalize(superTy), scope);
if (semantic.isSubtype)
if (FFlag::LuauSubtypingStopAtNormFail && semantic.normalizationTooComplex)
{
result = semantic;
}
else if (semantic.isSubtype)
{
// Clear the semantic reasoning, as any reasonings within
// potentially contain invalid paths.
@ -1082,6 +1101,10 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypeId sub
for (TypeId ty : superUnion)
{
SubtypingResult next = isCovariantWith(env, subTy, ty, scope);
if (FFlag::LuauSubtypingStopAtNormFail && next.normalizationTooComplex)
return SubtypingResult{false, /* normalizationTooComplex */ true};
if (next.isSubtype)
return SubtypingResult{true};
}
@ -1100,7 +1123,13 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const Unio
std::vector<SubtypingResult> subtypings;
size_t i = 0;
for (TypeId ty : subUnion)
{
subtypings.push_back(isCovariantWith(env, ty, superTy, scope).withSubComponent(TypePath::Index{i++, TypePath::Index::Variant::Union}));
if (FFlag::LuauSubtypingStopAtNormFail && subtypings.back().normalizationTooComplex)
return SubtypingResult{false, /* normalizationTooComplex */ true};
}
return SubtypingResult::all(subtypings);
}
@ -1110,7 +1139,13 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypeId sub
std::vector<SubtypingResult> subtypings;
size_t i = 0;
for (TypeId ty : superIntersection)
{
subtypings.push_back(isCovariantWith(env, subTy, ty, scope).withSuperComponent(TypePath::Index{i++, TypePath::Index::Variant::Intersection}));
if (FFlag::LuauSubtypingStopAtNormFail && subtypings.back().normalizationTooComplex)
return SubtypingResult{false, /* normalizationTooComplex */ true};
}
return SubtypingResult::all(subtypings);
}
@ -1120,7 +1155,13 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const Inte
std::vector<SubtypingResult> subtypings;
size_t i = 0;
for (TypeId ty : subIntersection)
{
subtypings.push_back(isCovariantWith(env, ty, superTy, scope).withSubComponent(TypePath::Index{i++, TypePath::Index::Variant::Intersection}));
if (FFlag::LuauSubtypingStopAtNormFail && subtypings.back().normalizationTooComplex)
return SubtypingResult{false, /* normalizationTooComplex */ true};
}
return SubtypingResult::any(subtypings);
}
@ -1410,7 +1451,7 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const Meta
// of the supertype table.
//
// There's a flaw here in that if the __index metamethod contributes a new
// field that would satisfy the subtyping relationship, we'll erronously say
// field that would satisfy the subtyping relationship, we'll erroneously say
// that the metatable isn't a subtype of the table, even though they have
// compatible properties/shapes. We'll revisit this later when we have a
// better understanding of how important this is.
@ -1760,7 +1801,12 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const Type
{
results.emplace_back();
for (TypeId superTy : superTypes)
{
results.back().orElse(isCovariantWith(env, subTy, superTy, scope));
if (FFlag::LuauSubtypingStopAtNormFail && results.back().normalizationTooComplex)
return SubtypingResult{false, /* normalizationTooComplex */ true};
}
}
return SubtypingResult::all(results);

View file

@ -14,6 +14,8 @@
#include "Luau/Unifier2.h"
LUAU_FASTFLAGVARIABLE(LuauBidirectionalInferenceUpcast)
LUAU_FASTFLAGVARIABLE(LuauBidirectionalInferenceCollectIndexerTypes)
LUAU_FASTFLAGVARIABLE(LuauBidirectionalFailsafe)
namespace Luau
{
@ -136,14 +138,13 @@ TypeId matchLiteralType(
* things like replace explicit named properties with indexers as required
* by the expected type.
*/
if (!isLiteral(expr))
{
if (FFlag::LuauBidirectionalInferenceUpcast)
{
auto result = subtyping->isSubtype(/*subTy=*/exprType, /*superTy=*/expectedType, unifier->scope);
return result.isSubtype
? expectedType
: exprType;
return result.isSubtype ? expectedType : exprType;
}
else
return exprType;
@ -152,11 +153,23 @@ TypeId matchLiteralType(
expectedType = follow(expectedType);
exprType = follow(exprType);
if (get<AnyType>(expectedType) || get<UnknownType>(expectedType))
if (FFlag::LuauBidirectionalInferenceCollectIndexerTypes)
{
// "Narrowing" to unknown or any is not going to do anything useful.
return exprType;
// The intent of `matchLiteralType` is to upcast values when it's safe
// to do so. it's always safe to upcast to `any` or `unknown`, so we
// can unconditionally do so here.
if (is<AnyType, UnknownType>(expectedType))
return expectedType;
}
else
{
if (get<AnyType>(expectedType) || get<UnknownType>(expectedType))
{
// "Narrowing" to unknown or any is not going to do anything useful.
return exprType;
}
}
if (expr->is<AstExprConstantString>())
{
@ -238,6 +251,15 @@ TypeId matchLiteralType(
if (auto exprTable = expr->as<AstExprTable>())
{
TableType* const tableTy = getMutable<TableType>(exprType);
// This can occur if we have an expression like:
//
// { x = {}, x = 42 }
//
// The type of this will be `{ x: number }`
if (FFlag::LuauBidirectionalFailsafe && !tableTy)
return exprType;
LUAU_ASSERT(tableTy);
const TableType* expectedTableTy = get<TableType>(expectedType);
@ -264,6 +286,9 @@ TypeId matchLiteralType(
DenseHashSet<AstExprConstantString*> keysToDelete{nullptr};
DenseHashSet<TypeId> indexerKeyTypes{nullptr};
DenseHashSet<TypeId> indexerValueTypes{nullptr};
for (const AstExprTable::Item& item : exprTable->items)
{
if (isRecord(item))
@ -271,6 +296,11 @@ TypeId matchLiteralType(
const AstArray<char>& s = item.key->as<AstExprConstantString>()->value;
std::string keyStr{s.data, s.data + s.size};
auto it = tableTy->props.find(keyStr);
// This can occur, potentially, if we are re-entrant.
if (FFlag::LuauBidirectionalFailsafe && it == tableTy->props.end())
continue;
LUAU_ASSERT(it != tableTy->props.end());
Property& prop = it->second;
@ -307,10 +337,18 @@ TypeId matchLiteralType(
toBlock
);
if (tableTy->indexer)
unifier->unify(matchedType, tableTy->indexer->indexResultType);
if (FFlag::LuauBidirectionalInferenceCollectIndexerTypes)
{
indexerKeyTypes.insert(arena->addType(SingletonType{StringSingleton{keyStr}}));
indexerValueTypes.insert(matchedType);
}
else
tableTy->indexer = TableIndexer{expectedTableTy->indexer->indexType, matchedType};
{
if (tableTy->indexer)
unifier->unify(matchedType, tableTy->indexer->indexResultType);
else
tableTy->indexer = TableIndexer{expectedTableTy->indexer->indexType, matchedType};
}
keysToDelete.insert(item.key->as<AstExprConstantString>());
@ -368,6 +406,11 @@ TypeId matchLiteralType(
LUAU_ASSERT(matchedType);
(*astExpectedTypes)[item.value] = matchedType;
// NOTE: We do *not* add to the potential indexer types here.
// I think this is correct to support something like:
//
// { [string]: number, foo: boolean }
//
}
else if (item.kind == AstExprTable::Item::List)
{
@ -392,9 +435,18 @@ TypeId matchLiteralType(
toBlock
);
// if the index result type is the prop type, we can replace it with the matched type here.
if (tableTy->indexer->indexResultType == *propTy)
tableTy->indexer->indexResultType = matchedType;
if (FFlag::LuauBidirectionalInferenceCollectIndexerTypes)
{
indexerKeyTypes.insert(builtinTypes->numberType);
indexerValueTypes.insert(matchedType);
}
else
{
// if the index result type is the prop type, we can replace it with the matched type here.
if (tableTy->indexer->indexResultType == *propTy)
tableTy->indexer->indexResultType = matchedType;
}
}
}
else if (item.kind == AstExprTable::Item::General)
@ -416,6 +468,13 @@ TypeId matchLiteralType(
// Populate expected types for non-string keys declared with [] (the code below will handle the case where they are strings)
if (!item.key->as<AstExprConstantString>() && expectedTableTy->indexer)
(*astExpectedTypes)[item.key] = expectedTableTy->indexer->indexType;
if (FFlag::LuauBidirectionalInferenceCollectIndexerTypes)
{
indexerKeyTypes.insert(tKey);
indexerValueTypes.insert(tProp);
}
}
else
LUAU_ASSERT(!"Unexpected");
@ -477,9 +536,39 @@ TypeId matchLiteralType(
// have one too.
// TODO: If the expected table also has an indexer, we might want to
// push the expected indexer's types into it.
if (expectedTableTy->indexer && !tableTy->indexer)
if (FFlag::LuauBidirectionalInferenceCollectIndexerTypes && expectedTableTy->indexer)
{
tableTy->indexer = expectedTableTy->indexer;
if (indexerValueTypes.size() > 0 && indexerKeyTypes.size() > 0)
{
TypeId inferredKeyType = builtinTypes->neverType;
TypeId inferredValueType = builtinTypes->neverType;
for (auto kt: indexerKeyTypes)
{
auto simplified = simplifyUnion(builtinTypes, arena, inferredKeyType, kt);
inferredKeyType = simplified.result;
}
for (auto vt: indexerValueTypes)
{
auto simplified = simplifyUnion(builtinTypes, arena, inferredValueType, vt);
inferredValueType = simplified.result;
}
tableTy->indexer = TableIndexer{inferredKeyType, inferredValueType};
auto keyCheck = subtyping->isSubtype(inferredKeyType, expectedTableTy->indexer->indexType, unifier->scope);
if (keyCheck.isSubtype)
tableTy->indexer->indexType = expectedTableTy->indexer->indexType;
auto valueCheck = subtyping->isSubtype(inferredValueType, expectedTableTy->indexer->indexResultType, unifier->scope);
if (valueCheck.isSubtype)
tableTy->indexer->indexResultType = expectedTableTy->indexer->indexResultType;
}
else
LUAU_ASSERT(indexerKeyTypes.empty() && indexerValueTypes.empty());
}
else
{
if (expectedTableTy->indexer && !tableTy->indexer)
{
tableTy->indexer = expectedTableTy->indexer;
}
}
}

View file

@ -10,11 +10,12 @@
#include <limits>
#include <math.h>
LUAU_FASTFLAG(LuauStoreCSTData)
LUAU_FASTFLAG(LuauStoreCSTData2)
LUAU_FASTFLAG(LuauExtendStatEndPosWithSemicolon)
LUAU_FASTFLAG(LuauAstTypeGroup3)
LUAU_FASTFLAG(LuauFixDoBlockEndLocation)
LUAU_FASTFLAG(LuauParseOptionalAsNode)
LUAU_FASTFLAG(LuauParseOptionalAsNode2)
LUAU_FASTFLAG(LuauFixFunctionWithAttributesStartLocation)
namespace
{
@ -167,7 +168,7 @@ struct StringWriter : Writer
void symbol(std::string_view s) override
{
if (FFlag::LuauStoreCSTData)
if (FFlag::LuauStoreCSTData2)
{
write(s);
}
@ -257,7 +258,7 @@ public:
first = !first;
else
{
if (FFlag::LuauStoreCSTData && commaPosition)
if (FFlag::LuauStoreCSTData2 && commaPosition)
{
writer.advance(*commaPosition);
commaPosition++;
@ -1229,9 +1230,18 @@ struct Printer_DEPRECATED
AstType* l = a->types.data[0];
AstType* r = a->types.data[1];
auto lta = l->as<AstTypeReference>();
if (lta && lta->name == "nil")
std::swap(l, r);
if (FFlag::LuauParseOptionalAsNode2)
{
auto lta = l->as<AstTypeReference>();
if (lta && lta->name == "nil" && !r->is<AstTypeOptional>())
std::swap(l, r);
}
else
{
auto lta = l->as<AstTypeReference>();
if (lta && lta->name == "nil")
std::swap(l, r);
}
// it's still possible that we had a (T | U) or (T | nil) and not (nil | T)
auto rta = r->as<AstTypeReference>();
@ -1254,7 +1264,7 @@ struct Printer_DEPRECATED
for (size_t i = 0; i < a->types.size; ++i)
{
if (FFlag::LuauParseOptionalAsNode)
if (FFlag::LuauParseOptionalAsNode2)
{
if (a->types.data[i]->is<AstTypeOptional>())
{
@ -1489,7 +1499,8 @@ struct Printer
void visualize(AstExpr& expr)
{
advance(expr.location.begin);
if (!expr.is<AstExprFunction>() || FFlag::LuauFixFunctionWithAttributesStartLocation)
advance(expr.location.begin);
if (const auto& a = expr.as<AstExprGroup>())
{
@ -1623,6 +1634,17 @@ struct Printer
}
else if (const auto& a = expr.as<AstExprFunction>())
{
for (const auto& attribute : a->attributes)
visualizeAttribute(*attribute);
if (FFlag::LuauFixFunctionWithAttributesStartLocation)
{
if (const auto cstNode = lookupCstNode<CstExprFunction>(a))
advance(cstNode->functionKeywordPosition);
}
else
{
advance(a->location.begin);
}
writer.keyword("function");
visualizeFunctionBody(*a);
}
@ -1874,7 +1896,8 @@ struct Printer
void visualize(AstStat& program)
{
advance(program.location.begin);
if ((!program.is<AstStatLocalFunction>() && !program.is<AstStatFunction>()) || FFlag::LuauFixFunctionWithAttributesStartLocation)
advance(program.location.begin);
if (const auto& block = program.as<AstStatBlock>())
{
@ -2111,13 +2134,36 @@ struct Printer
}
else if (const auto& a = program.as<AstStatFunction>())
{
for (const auto& attribute : a->func->attributes)
visualizeAttribute(*attribute);
if (FFlag::LuauFixFunctionWithAttributesStartLocation)
{
if (const auto cstNode = lookupCstNode<CstStatFunction>(a))
advance(cstNode->functionKeywordPosition);
}
else
{
advance(a->location.begin);
}
writer.keyword("function");
visualize(*a->name);
visualizeFunctionBody(*a->func);
}
else if (const auto& a = program.as<AstStatLocalFunction>())
{
for (const auto& attribute : a->func->attributes)
visualizeAttribute(*attribute);
const auto cstNode = lookupCstNode<CstStatLocalFunction>(a);
if (FFlag::LuauFixFunctionWithAttributesStartLocation)
{
if (cstNode)
advance(cstNode->localKeywordPosition);
}
else
{
advance(a->location.begin);
}
writer.keyword("local");
@ -2261,7 +2307,7 @@ struct Printer
if (program.hasSemicolon)
{
if (FFlag::LuauStoreCSTData)
if (FFlag::LuauStoreCSTData2)
advanceBefore(program.location.end, 1);
writer.symbol(";");
}
@ -2271,7 +2317,7 @@ struct Printer
{
const auto cstNode = lookupCstNode<CstExprFunction>(&func);
// TODO(CLI-139347): need to handle attributes, argument types, and return type (incl. parentheses of return type)
// TODO(CLI-139347): need to handle return type (incl. parentheses of return type)
if (func.generics.size > 0 || func.genericPacks.size > 0)
{
@ -2427,6 +2473,23 @@ struct Printer
}
}
void visualizeAttribute(AstAttr& attribute)
{
advance(attribute.location.begin);
switch (attribute.type)
{
case AstAttr::Checked:
writer.keyword("@checked");
break;
case AstAttr::Native:
writer.keyword("@native");
break;
case AstAttr::Deprecated:
writer.keyword("@deprecated");
break;
}
}
void visualizeTypeAnnotation(AstType& typeAnnotation)
{
advance(typeAnnotation.location.begin);
@ -2671,14 +2734,25 @@ struct Printer
}
else if (const auto& a = typeAnnotation.as<AstTypeUnion>())
{
if (a->types.size == 2)
const auto cstNode = lookupCstNode<CstTypeUnion>(a);
if (!cstNode && a->types.size == 2)
{
AstType* l = a->types.data[0];
AstType* r = a->types.data[1];
auto lta = l->as<AstTypeReference>();
if (lta && lta->name == "nil")
std::swap(l, r);
if (FFlag::LuauParseOptionalAsNode2)
{
auto lta = l->as<AstTypeReference>();
if (lta && lta->name == "nil" && !r->is<AstTypeOptional>())
std::swap(l, r);
}
else
{
auto lta = l->as<AstTypeReference>();
if (lta && lta->name == "nil")
std::swap(l, r);
}
// it's still possible that we had a (T | U) or (T | nil) and not (nil | T)
auto rta = r->as<AstTypeReference>();
@ -2699,12 +2773,20 @@ struct Printer
}
}
if (cstNode && cstNode->leadingPosition)
{
advance(*cstNode->leadingPosition);
writer.symbol("|");
}
size_t separatorIndex = 0;
for (size_t i = 0; i < a->types.size; ++i)
{
if (FFlag::LuauParseOptionalAsNode)
if (FFlag::LuauParseOptionalAsNode2)
{
if (a->types.data[i]->is<AstTypeOptional>())
if (const auto optional = a->types.data[i]->as<AstTypeOptional>())
{
advance(optional->location.begin);
writer.symbol("?");
continue;
}
@ -2712,11 +2794,18 @@ struct Printer
if (i > 0)
{
writer.maybeSpace(a->types.data[i]->location.begin, 2);
if (cstNode && FFlag::LuauParseOptionalAsNode2)
{
// separatorIndex is only valid if `?` is handled as an AstTypeOptional
advance(cstNode->separatorPositions.data[separatorIndex]);
separatorIndex++;
}
else
writer.maybeSpace(a->types.data[i]->location.begin, 2);
writer.symbol("|");
}
bool wrap = a->types.data[i]->as<AstTypeIntersection>() || a->types.data[i]->as<AstTypeFunction>();
bool wrap = !cstNode && (a->types.data[i]->as<AstTypeIntersection>() || a->types.data[i]->as<AstTypeFunction>());
if (wrap)
writer.symbol("(");
@ -2729,15 +2818,27 @@ struct Printer
}
else if (const auto& a = typeAnnotation.as<AstTypeIntersection>())
{
const auto cstNode = lookupCstNode<CstTypeIntersection>(a);
// If the sizes are equal, we know there is a leading & token
if (cstNode && cstNode->leadingPosition)
{
advance(*cstNode->leadingPosition);
writer.symbol("&");
}
for (size_t i = 0; i < a->types.size; ++i)
{
if (i > 0)
{
writer.maybeSpace(a->types.data[i]->location.begin, 2);
if (cstNode)
advance(cstNode->separatorPositions.data[i - 1]);
else
writer.maybeSpace(a->types.data[i]->location.begin, 2);
writer.symbol("&");
}
bool wrap = a->types.data[i]->as<AstTypeUnion>() || a->types.data[i]->as<AstTypeFunction>();
bool wrap = !cstNode && (a->types.data[i]->as<AstTypeUnion>() || a->types.data[i]->as<AstTypeFunction>());
if (wrap)
writer.symbol("(");
@ -2786,7 +2887,7 @@ std::string toString(AstNode* node)
StringWriter writer;
writer.pos = node->location.begin;
if (FFlag::LuauStoreCSTData)
if (FFlag::LuauStoreCSTData2)
{
Printer printer(writer, CstNodeMap{nullptr});
printer.writeTypes = true;
@ -2822,7 +2923,7 @@ void dump(AstNode* node)
std::string transpile(AstStatBlock& block, const CstNodeMap& cstNodeMap)
{
StringWriter writer;
if (FFlag::LuauStoreCSTData)
if (FFlag::LuauStoreCSTData2)
{
Printer(writer, cstNodeMap).visualizeBlock(block);
}
@ -2836,7 +2937,7 @@ std::string transpile(AstStatBlock& block, const CstNodeMap& cstNodeMap)
std::string transpileWithTypes(AstStatBlock& block, const CstNodeMap& cstNodeMap)
{
StringWriter writer;
if (FFlag::LuauStoreCSTData)
if (FFlag::LuauStoreCSTData2)
{
Printer printer(writer, cstNodeMap);
printer.writeTypes = true;

View file

@ -13,7 +13,7 @@
#include <string>
LUAU_FASTFLAG(LuauStoreCSTData)
LUAU_FASTFLAG(LuauStoreCSTData2)
static char* allocateString(Luau::Allocator& allocator, std::string_view contents)
{
@ -308,7 +308,7 @@ public:
if (el)
new (arg)
std::optional<AstArgumentName>(AstArgumentName(AstName(el->name.c_str()), FFlag::LuauStoreCSTData ? Location() : el->location));
std::optional<AstArgumentName>(AstArgumentName(AstName(el->name.c_str()), FFlag::LuauStoreCSTData2 ? Location() : el->location));
else
new (arg) std::optional<AstArgumentName>();
}

View file

@ -33,6 +33,7 @@ LUAU_FASTFLAG(DebugLuauMagicTypes)
LUAU_FASTFLAG(LuauFreeTypesMustHaveBounds)
LUAU_FASTFLAGVARIABLE(LuauImproveTypePathsInErrors)
LUAU_FASTFLAG(LuauUserTypeFunTypecheck)
LUAU_FASTFLAGVARIABLE(LuauTypeCheckerAcceptNumberConcats)
namespace Luau
{
@ -2229,10 +2230,21 @@ TypeId TypeChecker2::visit(AstExprBinary* expr, AstNode* overrideKey)
return builtinTypes->numberType;
case AstExprBinary::Op::Concat:
testIsSubtype(leftType, builtinTypes->stringType, expr->left->location);
testIsSubtype(rightType, builtinTypes->stringType, expr->right->location);
{
if (FFlag::LuauTypeCheckerAcceptNumberConcats)
{
const TypeId numberOrString = module->internalTypes.addType(UnionType{{builtinTypes->numberType, builtinTypes->stringType}});
testIsSubtype(leftType, numberOrString, expr->left->location);
testIsSubtype(rightType, numberOrString, expr->right->location);
}
else
{
testIsSubtype(leftType, builtinTypes->stringType, expr->left->location);
testIsSubtype(rightType, builtinTypes->stringType, expr->right->location);
}
return builtinTypes->stringType;
}
case AstExprBinary::Op::CompareGe:
case AstExprBinary::Op::CompareGt:
case AstExprBinary::Op::CompareLe:

View file

@ -48,22 +48,28 @@ LUAU_DYNAMIC_FASTINTVARIABLE(LuauTypeFamilyUseGuesserDepth, -1);
LUAU_FASTFLAGVARIABLE(DebugLuauLogTypeFamilies)
LUAU_FASTFLAG(DebugLuauEqSatSimplification)
LUAU_FASTFLAG(LuauTypeFunResultInAutocomplete)
LUAU_FASTFLAGVARIABLE(LuauMetatableTypeFunctions)
LUAU_FASTFLAGVARIABLE(LuauClipNestedAndRecursiveUnion)
LUAU_FASTFLAGVARIABLE(LuauIndexTypeFunctionImprovements)
LUAU_FASTFLAGVARIABLE(LuauIndexTypeFunctionFunctionMetamethods)
LUAU_FASTFLAGVARIABLE(LuauIntersectNotNil)
LUAU_FASTFLAGVARIABLE(LuauSkipNoRefineDuringRefinement)
LUAU_FASTFLAGVARIABLE(LuauMetatablesHaveLength)
LUAU_FASTFLAGVARIABLE(LuauDontForgetToReduceUnionFunc)
LUAU_FASTFLAGVARIABLE(LuauSearchForRefineableType)
LUAU_FASTFLAGVARIABLE(LuauIndexAnyIsAny)
LUAU_FASTFLAGVARIABLE(LuauSimplyRefineNotNil)
LUAU_FASTFLAGVARIABLE(LuauIndexDeferPendingIndexee)
LUAU_FASTFLAGVARIABLE(LuauNewTypeFunReductionChecks2)
LUAU_FASTFLAGVARIABLE(LuauReduceUnionFollowUnionType)
namespace Luau
{
using TypeOrTypePackIdSet = DenseHashSet<const void*>;
struct InstanceCollector : TypeOnceVisitor
struct InstanceCollector_DEPRECATED : TypeOnceVisitor
{
VecDeque<TypeId> tys;
VecDeque<TypePackId> tps;
@ -118,6 +124,153 @@ struct InstanceCollector : TypeOnceVisitor
}
};
struct InstanceCollector : TypeOnceVisitor
{
DenseHashSet<TypeId> recordedTys{nullptr};
VecDeque<TypeId> tys;
DenseHashSet<TypePackId> recordedTps{nullptr};
VecDeque<TypePackId> tps;
TypeOrTypePackIdSet shouldGuess{nullptr};
std::vector<const void*> typeFunctionInstanceStack;
std::vector<TypeId> cyclicInstance;
bool visit(TypeId ty, const TypeFunctionInstanceType& tfit) override
{
// TypeVisitor performs a depth-first traversal in the absence of
// cycles. This means that by pushing to the front of the queue, we will
// try to reduce deeper instances first if we start with the first thing
// in the queue. Consider Add<Add<Add<number, number>, number>, number>:
// we want to reduce the innermost Add<number, number> instantiation
// first.
typeFunctionInstanceStack.push_back(ty);
if (DFInt::LuauTypeFamilyUseGuesserDepth >= 0 && int(typeFunctionInstanceStack.size()) > DFInt::LuauTypeFamilyUseGuesserDepth)
shouldGuess.insert(ty);
if (!recordedTys.contains(ty))
{
recordedTys.insert(ty);
tys.push_front(ty);
}
for (TypeId p : tfit.typeArguments)
traverse(p);
for (TypePackId p : tfit.packArguments)
traverse(p);
typeFunctionInstanceStack.pop_back();
return false;
}
void cycle(TypeId ty) override
{
TypeId t = follow(ty);
if (get<TypeFunctionInstanceType>(t))
{
// If we see a type a second time and it's in the type function stack, it's a real cycle
if (std::find(typeFunctionInstanceStack.begin(), typeFunctionInstanceStack.end(), t) != typeFunctionInstanceStack.end())
cyclicInstance.push_back(t);
}
}
bool visit(TypeId ty, const ClassType&) override
{
return false;
}
bool visit(TypePackId tp, const TypeFunctionInstanceTypePack& tfitp) override
{
// TypeVisitor performs a depth-first traversal in the absence of
// cycles. This means that by pushing to the front of the queue, we will
// try to reduce deeper instances first if we start with the first thing
// in the queue. Consider Add<Add<Add<number, number>, number>, number>:
// we want to reduce the innermost Add<number, number> instantiation
// first.
typeFunctionInstanceStack.push_back(tp);
if (DFInt::LuauTypeFamilyUseGuesserDepth >= 0 && int(typeFunctionInstanceStack.size()) > DFInt::LuauTypeFamilyUseGuesserDepth)
shouldGuess.insert(tp);
if (!recordedTps.contains(tp))
{
recordedTps.insert(tp);
tps.push_front(tp);
}
for (TypeId p : tfitp.typeArguments)
traverse(p);
for (TypePackId p : tfitp.packArguments)
traverse(p);
typeFunctionInstanceStack.pop_back();
return false;
}
};
struct UnscopedGenericFinder : TypeOnceVisitor
{
std::vector<TypeId> scopeGenTys;
std::vector<TypePackId> scopeGenTps;
bool foundUnscoped = false;
bool visit(TypeId ty) override
{
// Once we have found an unscoped generic, we will stop the traversal
return !foundUnscoped;
}
bool visit(TypePackId tp) override
{
// Once we have found an unscoped generic, we will stop the traversal
return !foundUnscoped;
}
bool visit(TypeId ty, const GenericType&) override
{
if (std::find(scopeGenTys.begin(), scopeGenTys.end(), ty) == scopeGenTys.end())
foundUnscoped = true;
return false;
}
bool visit(TypePackId tp, const GenericTypePack&) override
{
if (std::find(scopeGenTps.begin(), scopeGenTps.end(), tp) == scopeGenTps.end())
foundUnscoped = true;
return false;
}
bool visit(TypeId ty, const FunctionType& ftv) override
{
size_t startTyCount = scopeGenTys.size();
size_t startTpCount = scopeGenTps.size();
scopeGenTys.insert(scopeGenTys.end(), ftv.generics.begin(), ftv.generics.end());
scopeGenTps.insert(scopeGenTps.end(), ftv.genericPacks.begin(), ftv.genericPacks.end());
traverse(ftv.argTypes);
traverse(ftv.retTypes);
scopeGenTys.resize(startTyCount);
scopeGenTps.resize(startTpCount);
return false;
}
bool visit(TypeId ty, const ClassType&) override
{
return false;
}
};
struct TypeFunctionReducer
{
TypeFunctionContext ctx;
@ -358,7 +511,6 @@ struct TypeFunctionReducer
return false;
}
void stepType()
{
TypeId subject = follow(queuedTys.front());
@ -372,6 +524,26 @@ struct TypeFunctionReducer
if (const TypeFunctionInstanceType* tfit = get<TypeFunctionInstanceType>(subject))
{
if (FFlag::LuauNewTypeFunReductionChecks2 && tfit->function->name == "user")
{
UnscopedGenericFinder finder;
finder.traverse(subject);
if (finder.foundUnscoped)
{
// Do not step into this type again
irreducible.insert(subject);
// Let the caller know this type will not become reducible
result.irreducibleTypes.insert(subject);
if (FFlag::DebugLuauLogTypeFamilies)
printf("Irreducible due to an unscoped generic type\n");
return;
}
}
SkipTestResult testCyclic = testForSkippability(subject);
if (!testParameters(subject, tfit) && testCyclic != SkipTestResult::CyclicTypeFunction)
@ -480,56 +652,114 @@ static FunctionGraphReductionResult reduceFunctionsInternal(
FunctionGraphReductionResult reduceTypeFunctions(TypeId entrypoint, Location location, TypeFunctionContext ctx, bool force)
{
InstanceCollector collector;
try
if (FFlag::LuauNewTypeFunReductionChecks2)
{
collector.traverse(entrypoint);
InstanceCollector collector;
try
{
collector.traverse(entrypoint);
}
catch (RecursionLimitException&)
{
return FunctionGraphReductionResult{};
}
if (collector.tys.empty() && collector.tps.empty())
return {};
return reduceFunctionsInternal(
std::move(collector.tys),
std::move(collector.tps),
std::move(collector.shouldGuess),
std::move(collector.cyclicInstance),
location,
ctx,
force
);
}
catch (RecursionLimitException&)
else
{
return FunctionGraphReductionResult{};
InstanceCollector_DEPRECATED collector;
try
{
collector.traverse(entrypoint);
}
catch (RecursionLimitException&)
{
return FunctionGraphReductionResult{};
}
if (collector.tys.empty() && collector.tps.empty())
return {};
return reduceFunctionsInternal(
std::move(collector.tys),
std::move(collector.tps),
std::move(collector.shouldGuess),
std::move(collector.cyclicInstance),
location,
ctx,
force
);
}
if (collector.tys.empty() && collector.tps.empty())
return {};
return reduceFunctionsInternal(
std::move(collector.tys),
std::move(collector.tps),
std::move(collector.shouldGuess),
std::move(collector.cyclicInstance),
location,
ctx,
force
);
}
FunctionGraphReductionResult reduceTypeFunctions(TypePackId entrypoint, Location location, TypeFunctionContext ctx, bool force)
{
InstanceCollector collector;
try
if (FFlag::LuauNewTypeFunReductionChecks2)
{
collector.traverse(entrypoint);
InstanceCollector collector;
try
{
collector.traverse(entrypoint);
}
catch (RecursionLimitException&)
{
return FunctionGraphReductionResult{};
}
if (collector.tys.empty() && collector.tps.empty())
return {};
return reduceFunctionsInternal(
std::move(collector.tys),
std::move(collector.tps),
std::move(collector.shouldGuess),
std::move(collector.cyclicInstance),
location,
ctx,
force
);
}
catch (RecursionLimitException&)
else
{
return FunctionGraphReductionResult{};
InstanceCollector_DEPRECATED collector;
try
{
collector.traverse(entrypoint);
}
catch (RecursionLimitException&)
{
return FunctionGraphReductionResult{};
}
if (collector.tys.empty() && collector.tps.empty())
return {};
return reduceFunctionsInternal(
std::move(collector.tys),
std::move(collector.tps),
std::move(collector.shouldGuess),
std::move(collector.cyclicInstance),
location,
ctx,
force
);
}
if (collector.tys.empty() && collector.tps.empty())
return {};
return reduceFunctionsInternal(
std::move(collector.tys),
std::move(collector.tps),
std::move(collector.shouldGuess),
std::move(collector.cyclicInstance),
location,
ctx,
force
);
}
bool isPending(TypeId ty, ConstraintSolver* solver)
@ -624,6 +854,42 @@ static std::optional<TypeFunctionReductionResult<TypeId>> tryDistributeTypeFunct
return std::nullopt;
}
struct FindUserTypeFunctionBlockers : TypeOnceVisitor
{
NotNull<TypeFunctionContext> ctx;
DenseHashSet<TypeId> blockingTypeMap{nullptr};
std::vector<TypeId> blockingTypes;
explicit FindUserTypeFunctionBlockers(NotNull<TypeFunctionContext> ctx)
: TypeOnceVisitor(/* skipBoundTypes */ true)
, ctx(ctx)
{
}
bool visit(TypeId ty) override
{
if (isPending(ty, ctx->solver))
{
if (!blockingTypeMap.contains(ty))
{
blockingTypeMap.insert(ty);
blockingTypes.push_back(ty);
}
}
return true;
}
bool visit(TypePackId tp) override
{
return true;
}
bool visit(TypeId ty, const ClassType&) override
{
return false;
}
};
TypeFunctionReductionResult<TypeId> userDefinedTypeFunction(
TypeId instance,
const std::vector<TypeId>& typeParams,
@ -646,21 +912,38 @@ TypeFunctionReductionResult<TypeId> userDefinedTypeFunction(
}
// If type functions cannot be evaluated because of errors in the code, we do not generate any additional ones
if (!ctx->typeFunctionRuntime->allowEvaluation)
if (!ctx->typeFunctionRuntime->allowEvaluation || (FFlag::LuauTypeFunResultInAutocomplete && typeFunction->userFuncData.definition->hasErrors))
return {ctx->builtins->errorRecoveryType(), Reduction::MaybeOk, {}, {}};
for (auto typeParam : typeParams)
if (FFlag::LuauNewTypeFunReductionChecks2)
{
TypeId ty = follow(typeParam);
FindUserTypeFunctionBlockers check{ctx};
// block if we need to
if (isPending(ty, ctx->solver))
return {std::nullopt, Reduction::MaybeOk, {ty}, {}};
for (auto typeParam : typeParams)
check.traverse(follow(typeParam));
if (!check.blockingTypes.empty())
return {std::nullopt, Reduction::MaybeOk, check.blockingTypes, {}};
}
else
{
for (auto typeParam : typeParams)
{
TypeId ty = follow(typeParam);
// block if we need to
if (isPending(ty, ctx->solver))
return {std::nullopt, Reduction::MaybeOk, {ty}, {}};
}
}
// Ensure that whole type function environment is registered
for (auto& [name, definition] : typeFunction->userFuncData.environment)
{
// Cannot evaluate if a potential dependency couldn't be parsed
if (FFlag::LuauTypeFunResultInAutocomplete && definition.first->hasErrors)
return {ctx->builtins->errorRecoveryType(), Reduction::MaybeOk, {}, {}};
if (std::optional<std::string> error = ctx->typeFunctionRuntime->registerFunction(definition.first))
{
// Failure to register at this point means that original definition had to error out and should not have been present in the
@ -874,7 +1157,16 @@ TypeFunctionReductionResult<TypeId> lenTypeFunction(
std::optional<TypeId> mmType = findMetatableEntry(ctx->builtins, dummy, operandTy, "__len", Location{});
if (!mmType)
{
if (FFlag::LuauMetatablesHaveLength)
{
// If we have a metatable type with no __len, this means we still have a table with default length function
if (get<MetatableType>(normalizedOperand))
return {ctx->builtins->numberType, Reduction::MaybeOk, {}, {}};
}
return {std::nullopt, Reduction::Erroneous, {}, {}};
}
mmType = follow(*mmType);
if (isPending(*mmType, ctx->solver))
@ -1004,6 +1296,10 @@ std::optional<std::string> TypeFunctionRuntime::registerFunction(AstStatTypeFunc
if (!allowEvaluation)
return std::nullopt;
// Do not evaluate type functions with parse errors inside
if (FFlag::LuauTypeFunResultInAutocomplete && function->hasErrors)
return std::nullopt;
prepareState();
lua_State* global = state.get();
@ -1046,7 +1342,6 @@ std::optional<std::string> TypeFunctionRuntime::registerFunction(AstStatTypeFunc
std::string bytecode = builder.getBytecode();
// Separate sandboxed thread for individual execution and private globals
lua_State* L = lua_newthread(global);
LuauTempThreadPopper popper(global);
@ -1923,6 +2218,18 @@ TypeFunctionReductionResult<TypeId> refineTypeFunction(
}
}
if (FFlag::LuauSimplyRefineNotNil)
{
if (auto negation = get<NegationType>(discriminant))
{
if (auto primitive = get<PrimitiveType>(follow(negation->ty)); primitive && primitive->type == PrimitiveType::NilType)
{
SimplifyResult result = simplifyIntersection(ctx->builtins, ctx->arena, target, discriminant);
return {result.result, {}};
}
}
}
// If the target type is a table, then simplification already implements the logic to deal with refinements properly since the
// type of the discriminant is guaranteed to only ever be an (arbitrarily-nested) table of a single property type.
if (get<TableType>(target))
@ -2030,6 +2337,29 @@ struct CollectUnionTypeOptions : TypeOnceVisitor
return false;
}
bool visit(TypeId ty, const UnionType& ut) override
{
if (FFlag::LuauReduceUnionFollowUnionType)
{
// If we have something like:
//
// union<A | B, C | D>
//
// We probably just want to consider this to be the same as
//
// union<A, B, C, D>
return true;
}
else
{
// Copy of the default visit method.
options.insert(ty);
if (isPending(ty, ctx->solver))
blockingTypes.insert(ty);
return false;
}
}
bool visit(TypeId ty, const TypeFunctionInstanceType& tfit) override
{
if (tfit.function->name != builtinTypeFunctions().unionFunc.name)
@ -2618,6 +2948,10 @@ TypeFunctionReductionResult<TypeId> indexFunctionImpl(
)
{
TypeId indexeeTy = follow(typeParams.at(0));
if (FFlag::LuauIndexDeferPendingIndexee && isPending(indexeeTy, ctx->solver))
return {std::nullopt, Reduction::MaybeOk, {indexeeTy}, {}};
std::shared_ptr<const NormalizedType> indexeeNormTy = ctx->normalizer->normalize(indexeeTy);
// if the indexee failed to normalize, we can't reduce, but know nothing about inhabitance.

View file

@ -14,7 +14,6 @@
#include <vector>
LUAU_DYNAMIC_FASTINT(LuauTypeFunctionSerdeIterationLimit)
LUAU_FASTFLAGVARIABLE(LuauTypeFunPrintFix)
LUAU_FASTFLAGVARIABLE(LuauTypeFunReadWriteParents)
namespace Luau
@ -1656,10 +1655,7 @@ static int print(lua_State* L)
const char* s = luaL_tolstring(L, i, &l); // convert to string using __tostring et al
if (i > 1)
{
if (FFlag::LuauTypeFunPrintFix)
result.append(1, '\t');
else
result.append('\t', 1);
result.append(1, '\t');
}
result.append(s, l);
lua_pop(L, 1);

View file

@ -650,27 +650,22 @@ struct FreeTypeSearcher : TypeVisitor
{
}
enum Polarity
{
Positive,
Negative,
Both,
};
Polarity polarity = Positive;
Polarity polarity = Polarity::Positive;
void flip()
{
switch (polarity)
{
case Positive:
polarity = Negative;
case Polarity::Positive:
polarity = Polarity::Negative;
break;
case Negative:
polarity = Positive;
case Polarity::Negative:
polarity = Polarity::Positive;
break;
case Both:
case Polarity::Mixed:
break;
default:
LUAU_ASSERT(!"Unreachable");
}
}
@ -681,7 +676,7 @@ struct FreeTypeSearcher : TypeVisitor
{
switch (polarity)
{
case Positive:
case Polarity::Positive:
{
if (seenPositive.contains(ty))
return true;
@ -689,7 +684,7 @@ struct FreeTypeSearcher : TypeVisitor
seenPositive.insert(ty);
return false;
}
case Negative:
case Polarity::Negative:
{
if (seenNegative.contains(ty))
return true;
@ -697,7 +692,7 @@ struct FreeTypeSearcher : TypeVisitor
seenNegative.insert(ty);
return false;
}
case Both:
case Polarity::Mixed:
{
if (seenPositive.contains(ty) && seenNegative.contains(ty))
return true;
@ -706,6 +701,8 @@ struct FreeTypeSearcher : TypeVisitor
seenNegative.insert(ty);
return false;
}
default:
LUAU_ASSERT(!"Unreachable");
}
return false;
@ -736,16 +733,18 @@ struct FreeTypeSearcher : TypeVisitor
switch (polarity)
{
case Positive:
case Polarity::Positive:
positiveTypes[ty]++;
break;
case Negative:
case Polarity::Negative:
negativeTypes[ty]++;
break;
case Both:
case Polarity::Mixed:
positiveTypes[ty]++;
negativeTypes[ty]++;
break;
default:
LUAU_ASSERT(!"Unreachable");
}
return true;
@ -760,16 +759,18 @@ struct FreeTypeSearcher : TypeVisitor
{
switch (polarity)
{
case Positive:
case Polarity::Positive:
positiveTypes[ty]++;
break;
case Negative:
case Polarity::Negative:
negativeTypes[ty]++;
break;
case Both:
case Polarity::Mixed:
positiveTypes[ty]++;
negativeTypes[ty]++;
break;
default:
LUAU_ASSERT(!"Unreachable");
}
}
@ -782,7 +783,7 @@ struct FreeTypeSearcher : TypeVisitor
LUAU_ASSERT(prop.isShared());
Polarity p = polarity;
polarity = Both;
polarity = Polarity::Mixed;
traverse(prop.type());
polarity = p;
}
@ -826,16 +827,18 @@ struct FreeTypeSearcher : TypeVisitor
switch (polarity)
{
case Positive:
case Polarity::Positive:
positiveTypes[tp]++;
break;
case Negative:
case Polarity::Negative:
negativeTypes[tp]++;
break;
case Both:
case Polarity::Mixed:
positiveTypes[tp]++;
negativeTypes[tp]++;
break;
default:
LUAU_ASSERT(!"Unreachable");
}
return true;

View file

@ -194,6 +194,7 @@ public:
{
Checked,
Native,
Deprecated,
};
AstAttr(const Location& location, Type type);
@ -453,6 +454,7 @@ public:
void visit(AstVisitor* visitor) override;
bool hasNativeAttribute() const;
bool hasAttribute(AstAttr::Type attributeType) const;
AstArray<AstAttr*> attributes;
AstArray<AstGenericType*> generics;
@ -890,14 +892,22 @@ class AstStatTypeFunction : public AstStat
public:
LUAU_RTTI(AstStatTypeFunction);
AstStatTypeFunction(const Location& location, const AstName& name, const Location& nameLocation, AstExprFunction* body, bool exported);
AstStatTypeFunction(
const Location& location,
const AstName& name,
const Location& nameLocation,
AstExprFunction* body,
bool exported,
bool hasErrors
);
void visit(AstVisitor* visitor) override;
AstName name;
Location nameLocation;
AstExprFunction* body;
bool exported;
AstExprFunction* body = nullptr;
bool exported = false;
bool hasErrors = false;
};
class AstStatDeclareGlobal : public AstStat
@ -950,6 +960,7 @@ public:
void visit(AstVisitor* visitor) override;
bool isCheckedFunction() const;
bool hasAttribute(AstAttr::Type attributeType) const;
AstArray<AstAttr*> attributes;
AstName name;
@ -1106,6 +1117,7 @@ public:
void visit(AstVisitor* visitor) override;
bool isCheckedFunction() const;
bool hasAttribute(AstAttr::Type attributeType) const;
AstArray<AstAttr*> attributes;
AstArray<AstGenericType*> generics;
@ -1459,6 +1471,10 @@ public:
{
return visit(static_cast<AstStat*>(node));
}
virtual bool visit(class AstStatTypeFunction* node)
{
return visit(static_cast<AstStat*>(node));
}
virtual bool visit(class AstStatDeclareFunction* node)
{
return visit(static_cast<AstStat*>(node));

View file

@ -112,6 +112,7 @@ public:
CstExprFunction();
Position functionKeywordPosition{0, 0};
Position openGenericsPosition{0,0};
AstArray<Position> genericsCommaPositions;
Position closeGenericsPosition{0,0};
@ -274,13 +275,24 @@ public:
Position opPosition;
};
class CstStatFunction : public CstNode
{
public:
LUAU_CST_RTTI(CstStatFunction)
explicit CstStatFunction(Position functionKeywordPosition);
Position functionKeywordPosition;
};
class CstStatLocalFunction : public CstNode
{
public:
LUAU_CST_RTTI(CstStatLocalFunction)
explicit CstStatLocalFunction(Position functionKeywordPosition);
explicit CstStatLocalFunction(Position localKeywordPosition, Position functionKeywordPosition);
Position localKeywordPosition;
Position functionKeywordPosition;
};
@ -421,6 +433,28 @@ public:
Position closePosition;
};
class CstTypeUnion : public CstNode
{
public:
LUAU_CST_RTTI(CstTypeUnion)
CstTypeUnion(std::optional<Position> leadingPosition, AstArray<Position> separatorPositions);
std::optional<Position> leadingPosition;
AstArray<Position> separatorPositions;
};
class CstTypeIntersection : public CstNode
{
public:
LUAU_CST_RTTI(CstTypeIntersection)
explicit CstTypeIntersection(std::optional<Position> leadingPosition, AstArray<Position> separatorPositions);
std::optional<Position> leadingPosition;
AstArray<Position> separatorPositions;
};
class CstTypeSingletonString : public CstNode
{
public:

View file

@ -125,7 +125,7 @@ private:
AstStat* parseFor();
// funcname ::= Name {`.' Name} [`:' Name]
AstExpr* parseFunctionName(Location start_DEPRECATED, bool& hasself, AstName& debugname);
AstExpr* parseFunctionName(bool& hasself, AstName& debugname);
// function funcname funcbody
LUAU_FORCEINLINE AstStat* parseFunctionStat(const AstArray<AstAttr*>& attributes = {nullptr, 0});
@ -157,7 +157,9 @@ private:
// type function Name ... end
AstStat* parseTypeFunction(const Location& start, bool exported, Position typeKeywordPosition);
AstDeclaredClassProp parseDeclaredClassMethod();
AstDeclaredClassProp parseDeclaredClassMethod(const AstArray<AstAttr*>& attributes);
AstDeclaredClassProp parseDeclaredClassMethod_DEPRECATED();
// `declare global' Name: Type |
// `declare function' Name`(' [parlist] `)' [`:` Type]
@ -228,9 +230,9 @@ private:
Position colonPosition;
};
TableIndexerResult parseTableIndexer(AstTableAccess access, std::optional<Location> accessLocation);
// Remove with FFlagLuauStoreCSTData
AstTableIndexer* parseTableIndexer_DEPRECATED(AstTableAccess access, std::optional<Location> accessLocation);
TableIndexerResult parseTableIndexer(AstTableAccess access, std::optional<Location> accessLocation, Lexeme begin);
// Remove with FFlagLuauStoreCSTData2
AstTableIndexer* parseTableIndexer_DEPRECATED(AstTableAccess access, std::optional<Location> accessLocation, Lexeme begin);
AstTypeOrPack parseFunctionType(bool allowPack, const AstArray<AstAttr*>& attributes);
AstType* parseFunctionTypeTail(

View file

@ -3,9 +3,24 @@
#include "Luau/Common.h"
LUAU_FASTFLAG(LuauDeprecatedAttribute);
namespace Luau
{
static bool hasAttributeInArray(const AstArray<AstAttr*> attributes, AstAttr::Type attributeType)
{
LUAU_ASSERT(FFlag::LuauDeprecatedAttribute);
for (const auto attribute : attributes)
{
if (attribute->type == attributeType)
return true;
}
return false;
}
static void visitTypeList(AstVisitor* visitor, const AstTypeList& list)
{
for (AstType* ty : list.types)
@ -277,6 +292,13 @@ bool AstExprFunction::hasNativeAttribute() const
return false;
}
bool AstExprFunction::hasAttribute(const AstAttr::Type attributeType) const
{
LUAU_ASSERT(FFlag::LuauDeprecatedAttribute);
return hasAttributeInArray(attributes, attributeType);
}
AstExprTable::AstExprTable(const Location& location, const AstArray<Item>& items)
: AstExpr(ClassIndex(), location)
, items(items)
@ -791,13 +813,15 @@ AstStatTypeFunction::AstStatTypeFunction(
const AstName& name,
const Location& nameLocation,
AstExprFunction* body,
bool exported
bool exported,
bool hasErrors
)
: AstStat(ClassIndex(), location)
, name(name)
, nameLocation(nameLocation)
, body(body)
, exported(exported)
, hasErrors(hasErrors)
{
}
@ -894,6 +918,13 @@ bool AstStatDeclareFunction::isCheckedFunction() const
return false;
}
bool AstStatDeclareFunction::hasAttribute(AstAttr::Type attributeType) const
{
LUAU_ASSERT(FFlag::LuauDeprecatedAttribute);
return hasAttributeInArray(attributes, attributeType);
}
AstStatDeclareClass::AstStatDeclareClass(
const Location& location,
const AstName& name,
@ -1057,6 +1088,13 @@ bool AstTypeFunction::isCheckedFunction() const
return false;
}
bool AstTypeFunction::hasAttribute(AstAttr::Type attributeType) const
{
LUAU_ASSERT(FFlag::LuauDeprecatedAttribute);
return hasAttributeInArray(attributes, attributeType);
}
AstTypeTypeof::AstTypeTypeof(const Location& location, AstExpr* expr)
: AstType(ClassIndex(), location)
, expr(expr)

View file

@ -129,12 +129,19 @@ CstStatCompoundAssign::CstStatCompoundAssign(Position opPosition)
{
}
CstStatLocalFunction::CstStatLocalFunction(Position functionKeywordPosition)
CstStatFunction::CstStatFunction(Position functionKeywordPosition)
: CstNode(CstClassIndex())
, functionKeywordPosition(functionKeywordPosition)
{
}
CstStatLocalFunction::CstStatLocalFunction(Position localKeywordPosition, Position functionKeywordPosition)
: CstNode(CstClassIndex())
, localKeywordPosition(localKeywordPosition)
, functionKeywordPosition(functionKeywordPosition)
{
}
CstGenericType::CstGenericType(std::optional<Position> defaultEqualsPosition)
: CstNode(CstClassIndex())
, defaultEqualsPosition(defaultEqualsPosition)
@ -221,6 +228,20 @@ CstTypeTypeof::CstTypeTypeof(Position openPosition, Position closePosition)
{
}
CstTypeUnion::CstTypeUnion(std::optional<Position> leadingPosition, AstArray<Position> separatorPositions)
: CstNode(CstClassIndex())
, leadingPosition(leadingPosition)
, separatorPositions(separatorPositions)
{
}
CstTypeIntersection::CstTypeIntersection(std::optional<Position> leadingPosition, AstArray<Position> separatorPositions)
: CstNode(CstClassIndex())
, leadingPosition(leadingPosition)
, separatorPositions(separatorPositions)
{
}
CstTypeSingletonString::CstTypeSingletonString(AstArray<char> sourceString, CstExprConstantString::QuoteStyle quoteStyle, unsigned int blockDepth)
: CstNode(CstClassIndex())
, sourceString(sourceString)

File diff suppressed because it is too large Load diff

View file

@ -169,7 +169,6 @@ target_sources(Luau.CodeGen PRIVATE
# Luau.Analysis Sources
target_sources(Luau.Analysis PRIVATE
Analysis/include/Luau/Anyification.h
Analysis/include/Luau/AnyTypeSummary.h
Analysis/include/Luau/ApplyTypeFunction.h
Analysis/include/Luau/AstJsonEncoder.h
Analysis/include/Luau/AstQuery.h
@ -248,7 +247,6 @@ target_sources(Luau.Analysis PRIVATE
Analysis/include/Luau/VisitType.h
Analysis/src/Anyification.cpp
Analysis/src/AnyTypeSummary.cpp
Analysis/src/ApplyTypeFunction.cpp
Analysis/src/AstJsonEncoder.cpp
Analysis/src/AstQuery.cpp
@ -433,7 +431,6 @@ endif()
if(TARGET Luau.UnitTest)
# Luau.UnitTest Sources
target_sources(Luau.UnitTest PRIVATE
tests/AnyTypeSummary.test.cpp
tests/AssemblyBuilderA64.test.cpp
tests/AssemblyBuilderX64.test.cpp
tests/AstJsonEncoder.test.cpp

View file

@ -134,6 +134,7 @@ int registerTypes(Luau::Frontend& frontend, Luau::GlobalTypes& globals, bool for
getMutable<TableType>(vector3MetaType)->props = {
{"__add", {makeFunction(arena, nullopt, {vector3InstanceType, vector3InstanceType}, {vector3InstanceType})}},
};
getMutable<TableType>(vector3MetaType)->state = TableState::Sealed;
globals.globalScope->exportedTypeBindings["Vector3"] = TypeFun{{}, vector3InstanceType};

File diff suppressed because it is too large Load diff

View file

@ -12,6 +12,7 @@
using namespace Luau;
LUAU_FASTFLAG(LuauAstTypeGroup3)
LUAU_FASTFLAG(LuauFixFunctionWithAttributesStartLocation)
struct JsonEncoderFixture
{
@ -440,7 +441,9 @@ TEST_CASE_FIXTURE(JsonEncoderFixture, "encode_AstAttr")
AstStat* expr = expectParseStatement("@checked function a(b) return c end");
std::string_view expected =
R"({"type":"AstStatFunction","location":"0,9 - 0,35","name":{"type":"AstExprGlobal","location":"0,18 - 0,19","global":"a"},"func":{"type":"AstExprFunction","location":"0,9 - 0,35","attributes":[{"type":"AstAttr","location":"0,0 - 0,8","name":"checked"}],"generics":[],"genericPacks":[],"args":[{"luauType":null,"name":"b","type":"AstLocal","location":"0,20 - 0,21"}],"vararg":false,"varargLocation":"0,0 - 0,0","body":{"type":"AstStatBlock","location":"0,22 - 0,32","hasEnd":true,"body":[{"type":"AstStatReturn","location":"0,23 - 0,31","list":[{"type":"AstExprGlobal","location":"0,30 - 0,31","global":"c"}]}]},"functionDepth":1,"debugname":"a"}})";
FFlag::LuauFixFunctionWithAttributesStartLocation
? R"({"type":"AstStatFunction","location":"0,0 - 0,35","name":{"type":"AstExprGlobal","location":"0,18 - 0,19","global":"a"},"func":{"type":"AstExprFunction","location":"0,0 - 0,35","attributes":[{"type":"AstAttr","location":"0,0 - 0,8","name":"checked"}],"generics":[],"genericPacks":[],"args":[{"luauType":null,"name":"b","type":"AstLocal","location":"0,20 - 0,21"}],"vararg":false,"varargLocation":"0,0 - 0,0","body":{"type":"AstStatBlock","location":"0,22 - 0,32","hasEnd":true,"body":[{"type":"AstStatReturn","location":"0,23 - 0,31","list":[{"type":"AstExprGlobal","location":"0,30 - 0,31","global":"c"}]}]},"functionDepth":1,"debugname":"a"}})"
: R"({"type":"AstStatFunction","location":"0,9 - 0,35","name":{"type":"AstExprGlobal","location":"0,18 - 0,19","global":"a"},"func":{"type":"AstExprFunction","location":"0,9 - 0,35","attributes":[{"type":"AstAttr","location":"0,0 - 0,8","name":"checked"}],"generics":[],"genericPacks":[],"args":[{"luauType":null,"name":"b","type":"AstLocal","location":"0,20 - 0,21"}],"vararg":false,"varargLocation":"0,0 - 0,0","body":{"type":"AstStatBlock","location":"0,22 - 0,32","hasEnd":true,"body":[{"type":"AstStatReturn","location":"0,23 - 0,31","list":[{"type":"AstExprGlobal","location":"0,30 - 0,31","global":"c"}]}]},"functionDepth":1,"debugname":"a"}})";
CHECK(toJson(expr) == expected);
}

View file

@ -23,6 +23,7 @@ LUAU_FASTINT(LuauTypeInferRecursionLimit)
LUAU_FASTFLAG(LuauExposeRequireByStringAutocomplete)
LUAU_FASTFLAG(LuauAutocompleteUnionCopyPreviousSeen)
LUAU_FASTFLAG(LuauUserTypeFunTypecheck)
LUAU_FASTFLAG(LuauTypeFunResultInAutocomplete)
using namespace Luau;
@ -4496,4 +4497,27 @@ this@2
CHECK_EQ(ac.entryMap.count("thisShouldBeThere"), 0);
}
TEST_CASE_FIXTURE(ACBuiltinsFixture, "type_function_eval_in_autocomplete")
{
ScopedFastFlag newSolver{FFlag::LuauSolverV2, true};
ScopedFastFlag luauTypeFunResultInAutocomplete{FFlag::LuauTypeFunResultInAutocomplete, true};
check(R"(
type function foo(x)
local tbl = types.newtable(nil, nil, nil)
tbl:setproperty(types.singleton("boolean"), x)
tbl:setproperty(types.singleton("number"), types.number)
return tbl
end
local function test(a: foo<string>)
return a.@1
end
)");
auto ac = autocomplete('1');
CHECK_EQ(ac.entryMap.count("boolean"), 1);
CHECK_EQ(ac.entryMap.count("number"), 1);
}
TEST_SUITE_END();

View file

@ -372,7 +372,7 @@ LintResult Fixture::lint(const std::string& source, const std::optional<LintOpti
fileResolver.source[mm] = std::move(source);
frontend.markDirty(mm);
return lintModule(mm);
return lintModule(mm, lintOptions);
}
LintResult Fixture::lintModule(const ModuleName& moduleName, const std::optional<LintOptions>& lintOptions)

File diff suppressed because it is too large Load diff

View file

@ -9,6 +9,7 @@
LUAU_FASTFLAG(LuauSolverV2);
LUAU_FASTFLAG(LintRedundantNativeAttribute);
LUAU_FASTFLAG(LuauDeprecatedAttribute);
using namespace Luau;
@ -1600,6 +1601,326 @@ setfenv(h :: any, {})
CHECK_EQ(result.warnings[3].location.begin.line + 1, 11);
}
static void checkDeprecatedWarning(const Luau::LintWarning& warning, const Luau::Position& begin, const Luau::Position& end, const char* msg)
{
CHECK_EQ(warning.code, LintWarning::Code_DeprecatedApi);
CHECK_EQ(warning.location, Location(begin, end));
CHECK_EQ(warning.text, msg);
}
TEST_CASE_FIXTURE(Fixture, "DeprecatedAttribute")
{
ScopedFastFlag sff[] = {{FFlag::LuauDeprecatedAttribute, true}, {FFlag::LuauSolverV2, true}};
// @deprecated works on local functions
{
LintResult result = lint(R"(
@deprecated
local function testfun(x)
return x + 1
end
testfun(1)
)");
REQUIRE(1 == result.warnings.size());
checkDeprecatedWarning(result.warnings[0], Position(6, 0), Position(6, 7), "Function 'testfun' is deprecated");
}
// @deprecated works on globals functions
{
LintResult result = lint(R"(
@deprecated
function testfun(x)
return x + 1
end
testfun(1)
)");
REQUIRE(1 == result.warnings.size());
checkDeprecatedWarning(result.warnings[0], Position(6, 0), Position(6, 7), "Function 'testfun' is deprecated");
}
// @deprecated works on fully typed functions
{
LintResult result = lint(R"(
@deprecated
local function testfun(x:number):number
return x + 1
end
if math.random(2) == 2 then
testfun(1)
end
)");
REQUIRE(1 == result.warnings.size());
checkDeprecatedWarning(result.warnings[0], Position(7, 4), Position(7, 11), "Function 'testfun' is deprecated");
}
// @deprecated works on functions without an explicit return type
{
LintResult result = lint(R"(
@deprecated
local function testfun(x:number)
return x + 1
end
g(testfun)
)");
REQUIRE(1 == result.warnings.size());
checkDeprecatedWarning(result.warnings[0], Position(6, 2), Position(6, 9), "Function 'testfun' is deprecated");
}
// @deprecated works on functions without an explicit argument type
{
LintResult result = lint(R"(
@deprecated
local function testfun(x):number
if x == 1 then
return x
else
return 1 + testfun(x - 1)
end
end
testfun(1)
)");
REQUIRE(1 == result.warnings.size());
checkDeprecatedWarning(result.warnings[0], Position(10, 0), Position(10, 7), "Function 'testfun' is deprecated");
}
// @deprecated works on inner functions
{
LintResult result = lint(R"(
function flipFlop()
local state = false
@deprecated
local function invert()
state = !state
return state
end
return invert
end
f = flipFlop()
assert(f() == true)
)");
REQUIRE(2 == result.warnings.size());
checkDeprecatedWarning(result.warnings[0], Position(10, 11), Position(10, 17), "Function 'invert' is deprecated");
checkDeprecatedWarning(result.warnings[1], Position(14, 7), Position(14, 8), "Function 'f' is deprecated");
}
// @deprecated does not automatically apply to inner functions
{
LintResult result = lint(R"(
@deprecated
function flipFlop()
local state = false
local function invert()
state = !state
return state
end
return invert
end
f = flipFlop()
assert(f() == true)
)");
REQUIRE(1 == result.warnings.size());
checkDeprecatedWarning(result.warnings[0], Position(13, 4), Position(13, 12), "Function 'flipFlop' is deprecated");
}
// @deprecated works correctly if deprecated function is shadowed
{
LintResult result = lint(R"(
@deprecated
local function doTheThing()
print("doing")
end
doTheThing()
local function shadow()
local function doTheThing()
print("doing!")
end
doTheThing()
end
shadow()
)");
REQUIRE(1 == result.warnings.size());
checkDeprecatedWarning(result.warnings[0], Position(6, 0), Position(6, 10), "Function 'doTheThing' is deprecated");
}
// @deprecated does not issue warnings if a deprecated function uses itself
{
LintResult result = lint(R"(
@deprecated
function fibonacci(n)
if n == 0 then
return 0
elseif n == 1 then
return 1
else
return fibonacci(n - 1) + fibonacci(n - 2)
end
end
fibonacci(5)
)");
REQUIRE(1 == result.warnings.size());
checkDeprecatedWarning(result.warnings[0], Position(12, 0), Position(12, 9), "Function 'fibonacci' is deprecated");
}
// @deprecated works for mutually recursive functions
{
LintResult result = lint(R"(
@deprecated
function odd(x)
if x == 0 then
return false
else
return even(x - 1)
end
end
@deprecated
function even(x)
if x == 0 then
return true
else
return odd(x - 1)
end
end
assert(odd(1) == true)
assert(even(0) == true)
)");
REQUIRE(4 == result.warnings.size());
checkDeprecatedWarning(result.warnings[0], Position(6, 15), Position(6, 19), "Function 'even' is deprecated");
checkDeprecatedWarning(result.warnings[1], Position(15, 15), Position(15, 18), "Function 'odd' is deprecated");
checkDeprecatedWarning(result.warnings[2], Position(19, 7), Position(19, 10), "Function 'odd' is deprecated");
checkDeprecatedWarning(result.warnings[3], Position(20, 7), Position(20, 11), "Function 'even' is deprecated");
}
// @deprecated works for methods with a literal class name
{
LintResult result = lint(R"(
Account = { balance=0 }
@deprecated
function Account:deposit(v)
self.balance = self.balance + v
end
Account:deposit(200.00)
)");
REQUIRE(1 == result.warnings.size());
checkDeprecatedWarning(result.warnings[0], Position(8, 0), Position(8, 15), "Member 'Account.deposit' is deprecated");
}
// @deprecated works for methods with a compound expression class name
{
LintResult result = lint(R"(
Account = { balance=0 }
function getAccount()
return Account
end
@deprecated
function Account:deposit (v)
self.balance = self.balance + v
end
(getAccount()):deposit(200.00)
)");
REQUIRE(1 == result.warnings.size());
checkDeprecatedWarning(result.warnings[0], Position(12, 0), Position(12, 22), "Member 'deposit' is deprecated");
}
}
TEST_CASE_FIXTURE(Fixture, "DeprecatedAttributeFunctionDeclaration")
{
ScopedFastFlag sff[] = {{FFlag::LuauDeprecatedAttribute, true}, {FFlag::LuauSolverV2, true}};
// @deprecated works on function type declarations
loadDefinition(R"(
@deprecated declare function bar(x: number): string
)");
LintResult result = lint(R"(
bar(2)
)");
REQUIRE(1 == result.warnings.size());
checkDeprecatedWarning(result.warnings[0], Position(1, 0), Position(1, 3), "Function 'bar' is deprecated");
}
TEST_CASE_FIXTURE(Fixture, "DeprecatedAttributeTableDeclaration")
{
ScopedFastFlag sff[] = {{FFlag::LuauDeprecatedAttribute, true}, {FFlag::LuauSolverV2, true}};
// @deprecated works on table type declarations
loadDefinition(R"(
declare Hooty : {
tooty : @deprecated @checked (number) -> number
}
)");
LintResult result = lint(R"(
print(Hooty:tooty(2.0))
)");
REQUIRE(1 == result.warnings.size());
checkDeprecatedWarning(result.warnings[0], Position(1, 6), Position(1, 17), "Member 'Hooty.tooty' is deprecated");
}
TEST_CASE_FIXTURE(Fixture, "DeprecatedAttributeMethodDeclaration")
{
ScopedFastFlag sff[] = {{FFlag::LuauDeprecatedAttribute, true}, {FFlag::LuauSolverV2, true}};
// @deprecated works on table type declarations
loadDefinition(R"(
declare class Foo
@deprecated
function bar(self, value: number) : number
end
declare Foo: {
new: () -> Foo
}
)");
LintResult result = lint(R"(
local foo = Foo.new()
print(foo:bar(2.0))
)");
REQUIRE(1 == result.warnings.size());
checkDeprecatedWarning(result.warnings[0], Position(2, 6), Position(2, 13), "Member 'bar' is deprecated");
}
TEST_CASE_FIXTURE(BuiltinsFixture, "TableOperations")
{
LintResult result = lint(R"(

View file

@ -10,9 +10,15 @@
#include "Luau/Normalize.h"
#include "Luau/BuiltinDefinitions.h"
LUAU_FASTFLAG(LuauNormalizeNegatedErrorToAnError)
LUAU_FASTFLAG(LuauNormalizeIntersectErrorToAnError)
LUAU_FASTFLAG(LuauSolverV2)
LUAU_FASTINT(LuauTypeInferRecursionLimit)
LUAU_FASTFLAG(LuauNormalizeNegationFix)
LUAU_FASTINT(LuauNormalizeIntersectionLimit)
LUAU_FASTINT(LuauNormalizeUnionLimit)
LUAU_FASTFLAG(LuauNormalizeLimitFunctionSet)
LUAU_FASTFLAG(LuauSubtypingStopAtNormFail)
using namespace Luau;
namespace
@ -593,6 +599,25 @@ TEST_CASE_FIXTURE(NormalizeFixture, "intersect_truthy_expressed_as_intersection"
)")));
}
TEST_CASE_FIXTURE(NormalizeFixture, "intersect_error")
{
ScopedFastFlag luauNormalizeIntersectErrorToAnError{FFlag::LuauNormalizeIntersectErrorToAnError, true};
std::shared_ptr<const NormalizedType> norm = toNormalizedType(R"(string & AAA)", 1);
REQUIRE(norm);
CHECK("*error-type*" == toString(normalizer.typeFromNormal(*norm)));
}
TEST_CASE_FIXTURE(NormalizeFixture, "intersect_not_error")
{
ScopedFastFlag luauNormalizeIntersectErrorToAnError{FFlag::LuauNormalizeIntersectErrorToAnError, true};
ScopedFastFlag luauNormalizeNegatedErrorToAnError{FFlag::LuauNormalizeNegatedErrorToAnError, true};
std::shared_ptr<const NormalizedType> norm = toNormalizedType(R"(string & Not<)", 1);
REQUIRE(norm);
CHECK("*error-type*" == toString(normalizer.typeFromNormal(*norm)));
}
TEST_CASE_FIXTURE(NormalizeFixture, "union_of_union")
{
CHECK(R"("alpha" | "beta" | "gamma")" == toString(normal(R"(
@ -1032,7 +1057,6 @@ TEST_CASE_FIXTURE(NormalizeFixture, "free_type_and_not_truthy")
{
ScopedFastFlag sff[] = {
{FFlag::LuauSolverV2, true}, // Only because it affects the stringification of free types
{FFlag::LuauNormalizeNegationFix, true},
};
TypeId freeTy = arena.freshType(builtinTypes, &globalScope);
@ -1153,4 +1177,38 @@ end
)");
}
TEST_CASE_FIXTURE(BuiltinsFixture, "fuzz_limit_function_intersection_complexity")
{
ScopedFastInt luauNormalizeIntersectionLimit{FInt::LuauNormalizeIntersectionLimit, 50};
ScopedFastInt luauNormalizeUnionLimit{FInt::LuauNormalizeUnionLimit, 20};
ScopedFastFlag luauNormalizeLimitFunctionSet{FFlag::LuauNormalizeLimitFunctionSet, true};
ScopedFastFlag luauSubtypingStopAtNormFail{FFlag::LuauSubtypingStopAtNormFail, true};
CheckResult result = check(R"(
function _(_).readu32(l0)
return ({[_(_(_))]=_,[_(if _ then _)]=_,n0=_,})[_],nil
end
_(_)[_(n32)] %= _(_(_))
)");
LUAU_REQUIRE_ERRORS(result);
}
TEST_CASE_FIXTURE(BuiltinsFixture, "fuzz_propagate_normalization_failures")
{
ScopedFastInt luauNormalizeIntersectionLimit{FInt::LuauNormalizeIntersectionLimit, 50};
ScopedFastInt luauNormalizeUnionLimit{FInt::LuauNormalizeUnionLimit, 20};
ScopedFastFlag luauNormalizeLimitFunctionSet{FFlag::LuauNormalizeLimitFunctionSet, true};
ScopedFastFlag luauSubtypingStopAtNormFail{FFlag::LuauSubtypingStopAtNormFail, true};
CheckResult result = check(R"(
function _(_,"").readu32(l0)
return ({[_(_(_))]=_,[_(if _ then _,_())]=_,[""]=_,})[_],nil
end
_().readu32 %= _(_(_(_),_))
)");
LUAU_REQUIRE_ERRORS(result);
}
TEST_SUITE_END();

View file

@ -18,12 +18,17 @@ LUAU_FASTINT(LuauParseErrorLimit)
LUAU_FASTFLAG(LuauSolverV2)
LUAU_FASTFLAG(LuauAllowComplexTypesInGenericParams)
LUAU_FASTFLAG(LuauErrorRecoveryForTableTypes)
LUAU_FASTFLAG(LuauFixFunctionNameStartPosition)
LUAU_FASTFLAG(LuauExtendStatEndPosWithSemicolon)
LUAU_FASTFLAG(LuauPreserveUnionIntersectionNodeForLeadingTokenSingleType)
LUAU_FASTFLAG(LuauAstTypeGroup3)
LUAU_FASTFLAG(LuauFixDoBlockEndLocation)
LUAU_FASTFLAG(LuauParseOptionalAsNode)
LUAU_FASTFLAG(LuauParseOptionalAsNode2)
LUAU_FASTFLAG(LuauParseStringIndexer)
LUAU_FASTFLAG(LuauFixFunctionWithAttributesStartLocation)
LUAU_DYNAMIC_FASTFLAG(DebugLuauReportReturnTypeVariadicWithTypeSuffix)
// Clip with DebugLuauReportReturnTypeVariadicWithTypeSuffix
extern bool luau_telemetry_parsed_return_type_variadic_with_type_suffix;
namespace
{
@ -2541,6 +2546,40 @@ TEST_CASE_FIXTURE(Fixture, "do_block_end_location_is_after_end_token")
CHECK_EQ(block->location, Location{{1, 8}, {3, 11}});
}
TEST_CASE_FIXTURE(Fixture, "function_start_locations_are_before_attributes")
{
ScopedFastFlag _{FFlag::LuauFixFunctionWithAttributesStartLocation, true};
AstStatBlock* stat = parse(R"(
@native
function globalFunction()
end
@native
local function localFunction()
end
local _ = @native function()
end
)");
REQUIRE(stat);
REQUIRE_EQ(3, stat->body.size);
auto globalFunction = stat->body.data[0]->as<AstStatFunction>();
REQUIRE(globalFunction);
CHECK_EQ(globalFunction->location, Location({1, 8}, {3, 11}));
auto localFunction = stat->body.data[1]->as<AstStatLocalFunction>();
REQUIRE(localFunction);
CHECK_EQ(localFunction->location, Location({5, 8}, {7, 11}));
auto localVariable = stat->body.data[2]->as<AstStatLocal>();
REQUIRE(localVariable);
REQUIRE_EQ(localVariable->values.size, 1);
auto anonymousFunction = localVariable->values.data[0]->as<AstExprFunction>();
CHECK_EQ(anonymousFunction->location, Location({9, 18}, {10, 11}));
}
TEST_SUITE_END();
TEST_SUITE_BEGIN("ParseErrorRecovery");
@ -3819,7 +3858,7 @@ TEST_CASE_FIXTURE(Fixture, "grouped_function_type")
}
else
CHECK(unionTy->types.data[0]->is<AstTypeFunction>()); // () -> ()
if (FFlag::LuauParseOptionalAsNode)
if (FFlag::LuauParseOptionalAsNode2)
CHECK(unionTy->types.data[1]->is<AstTypeOptional>()); // ?
else
CHECK(unionTy->types.data[1]->is<AstTypeReference>()); // nil
@ -3880,7 +3919,6 @@ TEST_CASE_FIXTURE(Fixture, "recover_from_bad_table_type")
TEST_CASE_FIXTURE(Fixture, "function_name_has_correct_start_location")
{
ScopedFastFlag _{FFlag::LuauFixFunctionNameStartPosition, true};
AstStatBlock* block = parse(R"(
function simple()
end
@ -3929,6 +3967,8 @@ TEST_CASE_FIXTURE(Fixture, "stat_end_includes_semicolon_position")
TEST_CASE_FIXTURE(Fixture, "parsing_type_suffix_for_return_type_with_variadic")
{
ScopedFastFlag sff{DFFlag::DebugLuauReportReturnTypeVariadicWithTypeSuffix, true};
ParseResult result = tryParse(R"(
function foo(): (string, ...number) | boolean
end
@ -3936,7 +3976,13 @@ TEST_CASE_FIXTURE(Fixture, "parsing_type_suffix_for_return_type_with_variadic")
// TODO(CLI-140667): this should produce a ParseError in future when we fix the invalid syntax
CHECK(result.errors.size() == 0);
CHECK_EQ(luau_telemetry_parsed_return_type_variadic_with_type_suffix, true);
}
TEST_CASE_FIXTURE(Fixture, "parsing_string_union_indexers")
{
ScopedFastFlag _{FFlag::LuauParseStringIndexer, true};
parse(R"(type foo = { ["bar" | "baz"]: number })");
}
TEST_SUITE_END();

View file

@ -3,7 +3,7 @@
#include "Luau/Common.h"
#include <string.h>
#include <vector>
template<typename T>
struct [[nodiscard]] ScopedFValue
@ -49,4 +49,4 @@ public:
};
using ScopedFastFlag = ScopedFValue<bool>;
using ScopedFastInt = ScopedFValue<int>;
using ScopedFastInt = ScopedFValue<int>;

View file

@ -12,9 +12,11 @@
using namespace Luau;
LUAU_FASTFLAG(LuauStoreCSTData)
LUAU_FASTFLAG(LuauStoreCSTData2)
LUAU_FASTFLAG(LuauExtendStatEndPosWithSemicolon)
LUAU_FASTFLAG(LuauAstTypeGroup3);
LUAU_FASTFLAG(LuauPreserveUnionIntersectionNodeForLeadingTokenSingleType)
LUAU_FASTFLAG(LuauParseOptionalAsNode2)
TEST_SUITE_BEGIN("TranspilerTests");
@ -48,7 +50,7 @@ TEST_CASE("string_literals_containing_utf8")
TEST_CASE("if_stmt_spaces_around_tokens")
{
ScopedFastFlag _{FFlag::LuauStoreCSTData, true};
ScopedFastFlag _{FFlag::LuauStoreCSTData2, true};
const std::string one = R"( if This then Once() end)";
CHECK_EQ(one, transpile(one).code);
@ -97,7 +99,7 @@ TEST_CASE("elseif_chains_indent_sensibly")
TEST_CASE("strips_type_annotations")
{
const std::string code = R"( local s: string= 'hello there' )";
if (FFlag::LuauStoreCSTData)
if (FFlag::LuauStoreCSTData2)
{
const std::string expected = R"( local s = 'hello there' )";
CHECK_EQ(expected, transpile(code).code);
@ -112,7 +114,7 @@ TEST_CASE("strips_type_annotations")
TEST_CASE("strips_type_assertion_expressions")
{
const std::string code = R"( local s= some_function() :: any+ something_else() :: number )";
if (FFlag::LuauStoreCSTData)
if (FFlag::LuauStoreCSTData2)
{
const std::string expected = R"( local s= some_function() + something_else() )";
CHECK_EQ(expected, transpile(code).code);
@ -148,7 +150,7 @@ TEST_CASE("for_loop")
TEST_CASE("for_loop_spaces_around_tokens")
{
ScopedFastFlag _{FFlag::LuauStoreCSTData, true};
ScopedFastFlag _{FFlag::LuauStoreCSTData2, true};
const std::string one = R"( for index = 1, 10 do call(index) end )";
CHECK_EQ(one, transpile(one).code);
@ -173,7 +175,7 @@ TEST_CASE("for_in_loop")
TEST_CASE("for_in_loop_spaces_around_tokens")
{
ScopedFastFlag _{FFlag::LuauStoreCSTData, true};
ScopedFastFlag _{FFlag::LuauStoreCSTData2, true};
const std::string one = R"( for k, v in ipairs(x) do end )";
CHECK_EQ(one, transpile(one).code);
@ -198,7 +200,7 @@ TEST_CASE("while_loop")
TEST_CASE("while_loop_spaces_around_tokens")
{
ScopedFastFlag _{FFlag::LuauStoreCSTData, true};
ScopedFastFlag _{FFlag::LuauStoreCSTData2, true};
const std::string one = R"( while f(x) do print() end )";
CHECK_EQ(one, transpile(one).code);
@ -220,7 +222,7 @@ TEST_CASE("repeat_until_loop")
TEST_CASE("repeat_until_loop_condition_on_new_line")
{
ScopedFastFlag _{FFlag::LuauStoreCSTData, true};
ScopedFastFlag _{FFlag::LuauStoreCSTData2, true};
const std::string code = R"(
repeat
print()
@ -252,7 +254,7 @@ TEST_CASE("local_assignment")
TEST_CASE("local_assignment_spaces_around_tokens")
{
ScopedFastFlag _{FFlag::LuauStoreCSTData, true};
ScopedFastFlag _{FFlag::LuauStoreCSTData2, true};
const std::string one = R"( local x = 1 )";
CHECK_EQ(one, transpile(one).code);
@ -286,7 +288,7 @@ TEST_CASE("local_function")
TEST_CASE("local_function_spaces_around_tokens")
{
ScopedFastFlag _{FFlag::LuauStoreCSTData, true};
ScopedFastFlag _{FFlag::LuauStoreCSTData2, true};
const std::string one = R"( local function p(o, m, ...) end )";
CHECK_EQ(one, transpile(one).code);
@ -305,7 +307,7 @@ TEST_CASE("function")
TEST_CASE("function_spaces_around_tokens")
{
ScopedFastFlag _{FFlag::LuauStoreCSTData, true};
ScopedFastFlag _{FFlag::LuauStoreCSTData2, true};
const std::string two = R"( function p(o, m, ...) end )";
CHECK_EQ(two, transpile(two).code);
@ -333,7 +335,7 @@ TEST_CASE("function_spaces_around_tokens")
TEST_CASE("function_with_types_spaces_around_tokens")
{
ScopedFastFlag _{FFlag::LuauStoreCSTData, true};
ScopedFastFlag _{FFlag::LuauStoreCSTData2, true};
std::string code = R"( function p<X, Y, Z...>(o: string, m: number, ...: any): string end )";
CHECK_EQ(code, transpile(code, {}, true).code);
@ -394,7 +396,7 @@ TEST_CASE("function_with_types_spaces_around_tokens")
TEST_CASE("returns_spaces_around_tokens")
{
ScopedFastFlag _{FFlag::LuauStoreCSTData, true};
ScopedFastFlag _{FFlag::LuauStoreCSTData2, true};
const std::string one = R"( return 1 )";
CHECK_EQ(one, transpile(one).code);
@ -407,7 +409,7 @@ TEST_CASE("returns_spaces_around_tokens")
TEST_CASE_FIXTURE(Fixture, "type_alias_spaces_around_tokens")
{
ScopedFastFlag _{FFlag::LuauStoreCSTData, true};
ScopedFastFlag _{FFlag::LuauStoreCSTData2, true};
std::string code = R"( type Foo = string )";
CHECK_EQ(code, transpile(code, {}, true).code);
@ -456,7 +458,7 @@ TEST_CASE_FIXTURE(Fixture, "type_alias_spaces_around_tokens")
TEST_CASE_FIXTURE(Fixture, "type_alias_with_defaults_spaces_around_tokens")
{
ScopedFastFlag _{FFlag::LuauStoreCSTData, true};
ScopedFastFlag _{FFlag::LuauStoreCSTData2, true};
std::string code = R"( type Foo<X = string, Z... = ...any> = string )";
CHECK_EQ(code, transpile(code, {}, true).code);
@ -517,7 +519,7 @@ TEST_CASE("table_literal_closing_brace_at_correct_position")
TEST_CASE("table_literal_with_semicolon_separators")
{
ScopedFastFlag _{FFlag::LuauStoreCSTData, true};
ScopedFastFlag _{FFlag::LuauStoreCSTData2, true};
const std::string code = R"(
local t = { x = 1; y = 2 }
)";
@ -527,7 +529,7 @@ TEST_CASE("table_literal_with_semicolon_separators")
TEST_CASE("table_literal_with_trailing_separators")
{
ScopedFastFlag _{FFlag::LuauStoreCSTData, true};
ScopedFastFlag _{FFlag::LuauStoreCSTData2, true};
const std::string code = R"(
local t = { x = 1, y = 2, }
)";
@ -537,7 +539,7 @@ TEST_CASE("table_literal_with_trailing_separators")
TEST_CASE("table_literal_with_spaces_around_separator")
{
ScopedFastFlag _{FFlag::LuauStoreCSTData, true};
ScopedFastFlag _{FFlag::LuauStoreCSTData2, true};
const std::string code = R"(
local t = { x = 1 , y = 2 }
)";
@ -547,7 +549,7 @@ TEST_CASE("table_literal_with_spaces_around_separator")
TEST_CASE("table_literal_with_spaces_around_equals")
{
ScopedFastFlag _{FFlag::LuauStoreCSTData, true};
ScopedFastFlag _{FFlag::LuauStoreCSTData2, true};
const std::string code = R"(
local t = { x = 1 }
)";
@ -557,7 +559,7 @@ TEST_CASE("table_literal_with_spaces_around_equals")
TEST_CASE("table_literal_multiline_with_indexers")
{
ScopedFastFlag _{FFlag::LuauStoreCSTData, true};
ScopedFastFlag _{FFlag::LuauStoreCSTData2, true};
const std::string code = R"(
local t = {
["my first value"] = "x";
@ -585,7 +587,7 @@ TEST_CASE("spaces_between_keywords_even_if_it_pushes_the_line_estimation_off")
// Luau::Parser doesn't exactly preserve the string representation of numbers in Lua, so we can find ourselves
// falling out of sync with the original code. We need to push keywords out so that there's at least one space between them.
const std::string code = R"( if math.abs(raySlope) < .01 then return 0 end )";
if (FFlag::LuauStoreCSTData)
if (FFlag::LuauStoreCSTData2)
{
CHECK_EQ(code, transpile(code).code);
}
@ -605,7 +607,7 @@ TEST_CASE("numbers")
TEST_CASE("infinity")
{
const std::string code = R"( local a = 1e500 local b = 1e400 )";
if (FFlag::LuauStoreCSTData)
if (FFlag::LuauStoreCSTData2)
{
CHECK_EQ(code, transpile(code).code);
}
@ -618,21 +620,21 @@ TEST_CASE("infinity")
TEST_CASE("numbers_with_separators")
{
ScopedFastFlag _{FFlag::LuauStoreCSTData, true};
ScopedFastFlag _{FFlag::LuauStoreCSTData2, true};
const std::string code = R"( local a = 123_456_789 )";
CHECK_EQ(code, transpile(code).code);
}
TEST_CASE("hexadecimal_numbers")
{
ScopedFastFlag _{FFlag::LuauStoreCSTData, true};
ScopedFastFlag _{FFlag::LuauStoreCSTData2, true};
const std::string code = R"( local a = 0xFFFF )";
CHECK_EQ(code, transpile(code).code);
}
TEST_CASE("binary_numbers")
{
ScopedFastFlag _{FFlag::LuauStoreCSTData, true};
ScopedFastFlag _{FFlag::LuauStoreCSTData2, true};
const std::string code = R"( local a = 0b0101 )";
CHECK_EQ(code, transpile(code).code);
}
@ -645,28 +647,28 @@ TEST_CASE("single_quoted_strings")
TEST_CASE("double_quoted_strings")
{
ScopedFastFlag _{FFlag::LuauStoreCSTData, true};
ScopedFastFlag _{FFlag::LuauStoreCSTData2, true};
const std::string code = R"( local a = "hello world" )";
CHECK_EQ(code, transpile(code).code);
}
TEST_CASE("simple_interp_string")
{
ScopedFastFlag _{FFlag::LuauStoreCSTData, true};
ScopedFastFlag _{FFlag::LuauStoreCSTData2, true};
const std::string code = R"( local a = `hello world` )";
CHECK_EQ(code, transpile(code).code);
}
TEST_CASE("raw_strings")
{
ScopedFastFlag _{FFlag::LuauStoreCSTData, true};
ScopedFastFlag _{FFlag::LuauStoreCSTData2, true};
const std::string code = R"( local a = [[ hello world ]] )";
CHECK_EQ(code, transpile(code).code);
}
TEST_CASE("raw_strings_with_blocks")
{
ScopedFastFlag _{FFlag::LuauStoreCSTData, true};
ScopedFastFlag _{FFlag::LuauStoreCSTData2, true};
const std::string code = R"( local a = [==[ hello world ]==] )";
CHECK_EQ(code, transpile(code).code);
}
@ -685,7 +687,7 @@ TEST_CASE("escaped_strings_2")
TEST_CASE("escaped_strings_newline")
{
ScopedFastFlag _{FFlag::LuauStoreCSTData, true};
ScopedFastFlag _{FFlag::LuauStoreCSTData2, true};
const std::string code = R"(
print("foo \
bar")
@ -695,14 +697,14 @@ TEST_CASE("escaped_strings_newline")
TEST_CASE("escaped_strings_raw")
{
ScopedFastFlag _{FFlag::LuauStoreCSTData, true};
ScopedFastFlag _{FFlag::LuauStoreCSTData2, true};
const std::string code = R"( local x = [=[\v<((do|load)file|require)\s*\(?['"]\zs[^'"]+\ze['"]]=] )";
CHECK_EQ(code, transpile(code).code);
}
TEST_CASE("position_correctly_updated_when_writing_multiline_string")
{
ScopedFastFlag _{FFlag::LuauStoreCSTData, true};
ScopedFastFlag _{FFlag::LuauStoreCSTData2, true};
const std::string code = R"(
call([[
testing
@ -748,56 +750,56 @@ TEST_CASE("function_call_parentheses_multiple_args_no_space")
TEST_CASE("function_call_parentheses_multiple_args_space_before_commas")
{
ScopedFastFlag _{FFlag::LuauStoreCSTData, true};
ScopedFastFlag _{FFlag::LuauStoreCSTData2, true};
const std::string code = R"( call(arg1 ,arg3 ,arg3) )";
CHECK_EQ(code, transpile(code).code);
}
TEST_CASE("function_call_spaces_before_parentheses")
{
ScopedFastFlag _{FFlag::LuauStoreCSTData, true};
ScopedFastFlag _{FFlag::LuauStoreCSTData2, true};
const std::string code = R"( call () )";
CHECK_EQ(code, transpile(code).code);
}
TEST_CASE("function_call_spaces_within_parentheses")
{
ScopedFastFlag _{FFlag::LuauStoreCSTData, true};
ScopedFastFlag _{FFlag::LuauStoreCSTData2, true};
const std::string code = R"( call( ) )";
CHECK_EQ(code, transpile(code).code);
}
TEST_CASE("function_call_string_double_quotes")
{
ScopedFastFlag _{FFlag::LuauStoreCSTData, true};
ScopedFastFlag _{FFlag::LuauStoreCSTData2, true};
const std::string code = R"( call "string" )";
CHECK_EQ(code, transpile(code).code);
}
TEST_CASE("function_call_string_single_quotes")
{
ScopedFastFlag _{FFlag::LuauStoreCSTData, true};
ScopedFastFlag _{FFlag::LuauStoreCSTData2, true};
const std::string code = R"( call 'string' )";
CHECK_EQ(code, transpile(code).code);
}
TEST_CASE("function_call_string_no_space")
{
ScopedFastFlag _{FFlag::LuauStoreCSTData, true};
ScopedFastFlag _{FFlag::LuauStoreCSTData2, true};
const std::string code = R"( call'string' )";
CHECK_EQ(code, transpile(code).code);
}
TEST_CASE("function_call_table_literal")
{
ScopedFastFlag _{FFlag::LuauStoreCSTData, true};
ScopedFastFlag _{FFlag::LuauStoreCSTData2, true};
const std::string code = R"( call { x = 1 } )";
CHECK_EQ(code, transpile(code).code);
}
TEST_CASE("function_call_table_literal_no_space")
{
ScopedFastFlag _{FFlag::LuauStoreCSTData, true};
ScopedFastFlag _{FFlag::LuauStoreCSTData2, true};
const std::string code = R"( call{x=1} )";
CHECK_EQ(code, transpile(code).code);
}
@ -842,7 +844,7 @@ TEST_CASE("emit_a_do_block_in_cases_of_potentially_ambiguous_syntax")
TEST_CASE_FIXTURE(Fixture, "parentheses_multiline")
{
ScopedFastFlag _{FFlag::LuauStoreCSTData, true};
ScopedFastFlag _{FFlag::LuauStoreCSTData2, true};
std::string code = R"(
local test = (
x
@ -855,7 +857,7 @@ local test = (
TEST_CASE_FIXTURE(Fixture, "stmt_semicolon")
{
ScopedFastFlag flags[] = {
{FFlag::LuauStoreCSTData, true},
{FFlag::LuauStoreCSTData2, true},
{FFlag::LuauExtendStatEndPosWithSemicolon, true},
};
std::string code = R"( local test = 1; )";
@ -878,7 +880,7 @@ TEST_CASE_FIXTURE(Fixture, "do_block_ending_with_semicolon")
TEST_CASE_FIXTURE(Fixture, "if_stmt_semicolon")
{
ScopedFastFlag flags[] = {
{FFlag::LuauStoreCSTData, true},
{FFlag::LuauStoreCSTData2, true},
{FFlag::LuauExtendStatEndPosWithSemicolon, true},
};
std::string code = R"(
@ -892,7 +894,7 @@ TEST_CASE_FIXTURE(Fixture, "if_stmt_semicolon")
TEST_CASE_FIXTURE(Fixture, "if_stmt_semicolon_2")
{
ScopedFastFlag flags[] = {
{FFlag::LuauStoreCSTData, true},
{FFlag::LuauStoreCSTData2, true},
{FFlag::LuauExtendStatEndPosWithSemicolon, true},
};
std::string code = R"(
@ -904,7 +906,7 @@ TEST_CASE_FIXTURE(Fixture, "if_stmt_semicolon_2")
TEST_CASE_FIXTURE(Fixture, "for_loop_stmt_semicolon")
{
ScopedFastFlag flags[] = {
{FFlag::LuauStoreCSTData, true},
{FFlag::LuauStoreCSTData2, true},
{FFlag::LuauExtendStatEndPosWithSemicolon, true},
};
std::string code = R"(
@ -917,7 +919,7 @@ TEST_CASE_FIXTURE(Fixture, "for_loop_stmt_semicolon")
TEST_CASE_FIXTURE(Fixture, "while_do_semicolon")
{
ScopedFastFlag flags[] = {
{FFlag::LuauStoreCSTData, true},
{FFlag::LuauStoreCSTData2, true},
{FFlag::LuauExtendStatEndPosWithSemicolon, true},
};
std::string code = R"(
@ -930,7 +932,7 @@ TEST_CASE_FIXTURE(Fixture, "while_do_semicolon")
TEST_CASE_FIXTURE(Fixture, "function_definition_semicolon")
{
ScopedFastFlag flags[] = {
{FFlag::LuauStoreCSTData, true},
{FFlag::LuauStoreCSTData2, true},
{FFlag::LuauExtendStatEndPosWithSemicolon, true},
};
std::string code = R"(
@ -1011,7 +1013,7 @@ TEST_CASE("always_emit_a_space_after_local_keyword")
{
std::string code = "do local aZZZZ = Workspace.P1.Shape local bZZZZ = Enum.PartType.Cylinder end";
if (FFlag::LuauStoreCSTData)
if (FFlag::LuauStoreCSTData2)
{
CHECK_EQ(code, transpile(code).code);
}
@ -1056,7 +1058,7 @@ TEST_CASE_FIXTURE(Fixture, "type_lists_should_be_emitted_correctly")
end
)";
std::string expected = FFlag::LuauStoreCSTData ? R"(
std::string expected = FFlag::LuauStoreCSTData2 ? R"(
local a:(a:string,b:number,...string)->(string,...number)=function(a:string,b:number,...:string): (string,...number)
end
@ -1112,7 +1114,7 @@ TEST_CASE_FIXTURE(Fixture, "transpile_type_assertion")
TEST_CASE_FIXTURE(Fixture, "type_assertion_spaces_around_tokens")
{
ScopedFastFlag _{FFlag::LuauStoreCSTData, true};
ScopedFastFlag _{FFlag::LuauStoreCSTData2, true};
std::string code = "local a = 5 :: number";
CHECK_EQ(code, transpile(code, {}, true).code);
@ -1129,7 +1131,7 @@ TEST_CASE_FIXTURE(Fixture, "transpile_if_then_else")
TEST_CASE_FIXTURE(Fixture, "transpile_if_then_else_multiple_conditions")
{
ScopedFastFlag _{FFlag::LuauStoreCSTData, true};
ScopedFastFlag _{FFlag::LuauStoreCSTData2, true};
std::string code = "local a = if 1 then 2 elseif 3 then 4 else 5";
CHECK_EQ(code, transpile(code).code);
@ -1137,7 +1139,7 @@ TEST_CASE_FIXTURE(Fixture, "transpile_if_then_else_multiple_conditions")
TEST_CASE_FIXTURE(Fixture, "transpile_if_then_else_multiple_conditions_2")
{
ScopedFastFlag _{FFlag::LuauStoreCSTData, true};
ScopedFastFlag _{FFlag::LuauStoreCSTData2, true};
std::string code = R"(
local x = if yes
then nil
@ -1153,7 +1155,7 @@ TEST_CASE_FIXTURE(Fixture, "transpile_if_then_else_multiple_conditions_2")
TEST_CASE_FIXTURE(Fixture, "if_then_else_spaces_around_tokens")
{
ScopedFastFlag _{FFlag::LuauStoreCSTData, true};
ScopedFastFlag _{FFlag::LuauStoreCSTData2, true};
std::string code = "local a = if 1 then 2 else 3";
CHECK_EQ(code, transpile(code).code);
@ -1190,7 +1192,7 @@ TEST_CASE_FIXTURE(Fixture, "if_then_else_spaces_around_tokens")
TEST_CASE_FIXTURE(Fixture, "if_then_else_spaces_between_else_if")
{
ScopedFastFlag _{FFlag::LuauStoreCSTData, true};
ScopedFastFlag _{FFlag::LuauStoreCSTData2, true};
std::string code = R"(
return
if a then "was a" else
@ -1218,7 +1220,7 @@ local a: Import.Type
TEST_CASE_FIXTURE(Fixture, "transpile_type_reference_spaces_around_tokens")
{
ScopedFastFlag _{FFlag::LuauStoreCSTData, true};
ScopedFastFlag _{FFlag::LuauStoreCSTData2, true};
std::string code = R"( local _: Foo.Type )";
CHECK_EQ(code, transpile(code, {}, true).code);
@ -1257,7 +1259,7 @@ local b: Packed<(number, string)>
TEST_CASE_FIXTURE(Fixture, "type_packs_spaces_around_tokens")
{
ScopedFastFlag _{FFlag::LuauStoreCSTData, true};
ScopedFastFlag _{FFlag::LuauStoreCSTData2, true};
std::string code = R"( type _ = Packed< T...> )";
CHECK_EQ(code, transpile(code, {}, true).code);
@ -1316,7 +1318,9 @@ TEST_CASE_FIXTURE(Fixture, "transpile_union_type_nested_3")
{
std::string code = "local a: nil | (string & number)";
if (FFlag::LuauAstTypeGroup3)
if (FFlag::LuauStoreCSTData2)
CHECK_EQ(code, transpile(code, {}, true).code);
else if (FFlag::LuauAstTypeGroup3)
CHECK_EQ("local a: (string & number)?", transpile(code, {}, true).code);
else
CHECK_EQ("local a: ( string & number)?", transpile(code, {}, true).code);
@ -1336,6 +1340,117 @@ TEST_CASE_FIXTURE(Fixture, "transpile_intersection_type_nested_2")
CHECK_EQ(code, transpile(code, {}, true).code);
}
TEST_CASE_FIXTURE(Fixture, "transpile_leading_union_pipe")
{
ScopedFastFlag flags[] = {
{FFlag::LuauStoreCSTData2, true},
{FFlag::LuauPreserveUnionIntersectionNodeForLeadingTokenSingleType, true},
{FFlag::LuauParseOptionalAsNode2, true},
};
std::string code = "local a: | string | number";
CHECK_EQ(code, transpile(code, {}, true).code);
code = "local a: | string";
CHECK_EQ(code, transpile(code, {}, true).code);
}
TEST_CASE_FIXTURE(Fixture, "transpile_union_spaces_around_tokens")
{
ScopedFastFlag flags[] = {
{FFlag::LuauStoreCSTData2, true},
{FFlag::LuauPreserveUnionIntersectionNodeForLeadingTokenSingleType, true},
{FFlag::LuauParseOptionalAsNode2, true},
};
std::string code = "local a: string | number";
CHECK_EQ(code, transpile(code, {}, true).code);
code = "local a: string | number";
CHECK_EQ(code, transpile(code, {}, true).code);
}
TEST_CASE_FIXTURE(Fixture, "transpile_leading_intersection_ampersand")
{
ScopedFastFlag flags[] = {
{FFlag::LuauStoreCSTData2, true},
{FFlag::LuauPreserveUnionIntersectionNodeForLeadingTokenSingleType, true},
};
std::string code = "local a: & string & number";
CHECK_EQ(code, transpile(code, {}, true).code);
code = "local a: & string";
CHECK_EQ(code, transpile(code, {}, true).code);
}
TEST_CASE_FIXTURE(Fixture, "transpile_intersection_spaces_around_tokens")
{
ScopedFastFlag flags[] = {
{FFlag::LuauStoreCSTData2, true},
{FFlag::LuauPreserveUnionIntersectionNodeForLeadingTokenSingleType, true},
};
std::string code = "local a: string & number";
CHECK_EQ(code, transpile(code, {}, true).code);
code = "local a: string & number";
CHECK_EQ(code, transpile(code, {}, true).code);
}
TEST_CASE_FIXTURE(Fixture, "transpile_mixed_union_intersection")
{
ScopedFastFlag flags[] = {
{FFlag::LuauStoreCSTData2, true},
{FFlag::LuauAstTypeGroup3, true},
{FFlag::LuauParseOptionalAsNode2, true},
};
std::string code = "local a: string | (Foo & Bar)";
CHECK_EQ(code, transpile(code, {}, true).code);
code = "local a: string | (Foo & Bar)";
CHECK_EQ(code, transpile(code, {}, true).code);
code = "local a: string | ( Foo & Bar)";
CHECK_EQ(code, transpile(code, {}, true).code);
code = "local a: string | (Foo & Bar )";
CHECK_EQ(code, transpile(code, {}, true).code);
code = "local a: string & (Foo | Bar)";
CHECK_EQ(code, transpile(code, {}, true).code);
code = "local a: string & ( Foo | Bar)";
CHECK_EQ(code, transpile(code, {}, true).code);
code = "local a: string & (Foo | Bar )";
CHECK_EQ(code, transpile(code, {}, true).code);
}
TEST_CASE_FIXTURE(Fixture, "transpile_preserve_union_optional_style")
{
ScopedFastFlag flags[] = {
{FFlag::LuauStoreCSTData2, true},
{FFlag::LuauParseOptionalAsNode2, true},
};
std::string code = "local a: string | nil";
CHECK_EQ(code, transpile(code, {}, true).code);
code = "local a: string?";
CHECK_EQ(code, transpile(code, {}, true).code);
code = "local a: string???";
CHECK_EQ(code, transpile(code, {}, true).code);
code = "local a: string? | nil";
CHECK_EQ(code, transpile(code, {}, true).code);
code = "local a: string | nil | number";
CHECK_EQ(code, transpile(code, {}, true).code);
code = "local a: string | nil | number?";
CHECK_EQ(code, transpile(code, {}, true).code);
code = "local a: string? | number?";
CHECK_EQ(code, transpile(code, {}, true).code);
}
TEST_CASE_FIXTURE(Fixture, "transpile_varargs")
{
std::string code = "local function f(...) return ... end";
@ -1345,7 +1460,7 @@ TEST_CASE_FIXTURE(Fixture, "transpile_varargs")
TEST_CASE_FIXTURE(Fixture, "index_name_spaces_around_tokens")
{
ScopedFastFlag _{FFlag::LuauStoreCSTData, true};
ScopedFastFlag _{FFlag::LuauStoreCSTData2, true};
std::string one = "local _ = a.name";
CHECK_EQ(one, transpile(one, {}, true).code);
@ -1358,7 +1473,7 @@ TEST_CASE_FIXTURE(Fixture, "index_name_spaces_around_tokens")
TEST_CASE_FIXTURE(Fixture, "index_name_ends_with_digit")
{
ScopedFastFlag _{FFlag::LuauStoreCSTData, true};
ScopedFastFlag _{FFlag::LuauStoreCSTData2, true};
std::string code = "sparkles.Color = Color3.new()";
CHECK_EQ(code, transpile(code, {}, true).code);
}
@ -1372,7 +1487,7 @@ TEST_CASE_FIXTURE(Fixture, "transpile_index_expr")
TEST_CASE_FIXTURE(Fixture, "index_expr_spaces_around_tokens")
{
ScopedFastFlag _{FFlag::LuauStoreCSTData, true};
ScopedFastFlag _{FFlag::LuauStoreCSTData2, true};
std::string one = "local _ = a[2]";
CHECK_EQ(one, transpile(one, {}, true).code);
@ -1416,7 +1531,7 @@ local _ = # e
TEST_CASE_FIXTURE(Fixture, "binary_spaces_around_tokens")
{
ScopedFastFlag _{FFlag::LuauStoreCSTData, true};
ScopedFastFlag _{FFlag::LuauStoreCSTData2, true};
std::string code = R"(
local _ = 1+1
local _ = 1 +1
@ -1458,7 +1573,7 @@ a ..= ' - result'
TEST_CASE_FIXTURE(Fixture, "compound_assignment_spaces_around_tokens")
{
ScopedFastFlag _{FFlag::LuauStoreCSTData, true};
ScopedFastFlag _{FFlag::LuauStoreCSTData2, true};
std::string one = R"( a += 1 )";
CHECK_EQ(one, transpile(one, {}, true).code);
@ -1475,7 +1590,7 @@ TEST_CASE_FIXTURE(Fixture, "transpile_assign_multiple")
TEST_CASE_FIXTURE(Fixture, "transpile_assign_spaces_around_tokens")
{
ScopedFastFlag _{FFlag::LuauStoreCSTData, true};
ScopedFastFlag _{FFlag::LuauStoreCSTData2, true};
std::string one = "a = 1";
CHECK_EQ(one, transpile(one).code);
@ -1512,7 +1627,10 @@ TEST_CASE_FIXTURE(Fixture, "transpile_union_reverse")
{
std::string code = "local a: nil | number";
CHECK_EQ("local a: number?", transpile(code, {}, true).code);
if (FFlag::LuauStoreCSTData2)
CHECK_EQ(code, transpile(code, {}, true).code);
else
CHECK_EQ("local a: number?", transpile(code, {}, true).code);
}
TEST_CASE_FIXTURE(Fixture, "transpile_for_in_multiple")
@ -1628,7 +1746,7 @@ TEST_CASE_FIXTURE(Fixture, "transpile_for_in_multiple_types")
TEST_CASE_FIXTURE(Fixture, "transpile_string_interp")
{
ScopedFastFlag fflags[] = {
{FFlag::LuauStoreCSTData, true},
{FFlag::LuauStoreCSTData2, true},
};
std::string code = R"( local _ = `hello {name}` )";
@ -1638,7 +1756,7 @@ TEST_CASE_FIXTURE(Fixture, "transpile_string_interp")
TEST_CASE_FIXTURE(Fixture, "transpile_string_interp_multiline")
{
ScopedFastFlag fflags[] = {
{FFlag::LuauStoreCSTData, true},
{FFlag::LuauStoreCSTData2, true},
};
std::string code = R"( local _ = `hello {
name
@ -1650,7 +1768,7 @@ TEST_CASE_FIXTURE(Fixture, "transpile_string_interp_multiline")
TEST_CASE_FIXTURE(Fixture, "transpile_string_interp_on_new_line")
{
ScopedFastFlag fflags[] = {
{FFlag::LuauStoreCSTData, true},
{FFlag::LuauStoreCSTData2, true},
};
std::string code = R"(
error(
@ -1663,7 +1781,7 @@ TEST_CASE_FIXTURE(Fixture, "transpile_string_interp_on_new_line")
TEST_CASE_FIXTURE(Fixture, "transpile_string_interp_multiline_escape")
{
ScopedFastFlag _{FFlag::LuauStoreCSTData, true};
ScopedFastFlag _{FFlag::LuauStoreCSTData2, true};
std::string code = R"( local _ = `hello \
world!` )";
@ -1673,7 +1791,7 @@ TEST_CASE_FIXTURE(Fixture, "transpile_string_interp_multiline_escape")
TEST_CASE_FIXTURE(Fixture, "transpile_string_literal_escape")
{
ScopedFastFlag fflags[] = {
{FFlag::LuauStoreCSTData, true},
{FFlag::LuauStoreCSTData2, true},
};
std::string code = R"( local _ = ` bracket = \{, backtick = \` = {'ok'} ` )";
@ -1689,7 +1807,7 @@ TEST_CASE_FIXTURE(Fixture, "transpile_type_functions")
TEST_CASE_FIXTURE(Fixture, "transpile_type_functions_spaces_around_tokens")
{
ScopedFastFlag _{FFlag::LuauStoreCSTData, true};
ScopedFastFlag _{FFlag::LuauStoreCSTData2, true};
std::string code = R"( type function foo() end )";
CHECK_EQ(code, transpile(code, {}, true).code);
@ -1705,7 +1823,7 @@ TEST_CASE_FIXTURE(Fixture, "transpile_type_functions_spaces_around_tokens")
TEST_CASE_FIXTURE(Fixture, "transpile_typeof_spaces_around_tokens")
{
ScopedFastFlag _{FFlag::LuauStoreCSTData, true};
ScopedFastFlag _{FFlag::LuauStoreCSTData2, true};
std::string code = R"( type X = typeof(x) )";
CHECK_EQ(code, transpile(code, {}, true).code);
@ -1730,14 +1848,14 @@ TEST_CASE("transpile_single_quoted_string_types")
TEST_CASE("transpile_double_quoted_string_types")
{
ScopedFastFlag _{FFlag::LuauStoreCSTData, true};
ScopedFastFlag _{FFlag::LuauStoreCSTData2, true};
const std::string code = R"( type a = "hello world" )";
CHECK_EQ(code, transpile(code, {}, true).code);
}
TEST_CASE("transpile_raw_string_types")
{
ScopedFastFlag _{FFlag::LuauStoreCSTData, true};
ScopedFastFlag _{FFlag::LuauStoreCSTData2, true};
std::string code = R"( type a = [[ hello world ]] )";
CHECK_EQ(code, transpile(code, {}, true).code);
@ -1747,14 +1865,14 @@ TEST_CASE("transpile_raw_string_types")
TEST_CASE("transpile_escaped_string_types")
{
ScopedFastFlag _{FFlag::LuauStoreCSTData, true};
ScopedFastFlag _{FFlag::LuauStoreCSTData2, true};
const std::string code = R"( type a = "\\b\\t\\n\\\\" )";
CHECK_EQ(code, transpile(code, {}, true).code);
}
TEST_CASE("transpile_type_table_semicolon_separators")
{
ScopedFastFlag _{FFlag::LuauStoreCSTData, true};
ScopedFastFlag _{FFlag::LuauStoreCSTData2, true};
const std::string code = R"(
type Foo = {
bar: number;
@ -1766,7 +1884,7 @@ TEST_CASE("transpile_type_table_semicolon_separators")
TEST_CASE("transpile_type_table_access_modifiers")
{
ScopedFastFlag _{FFlag::LuauStoreCSTData, true};
ScopedFastFlag _{FFlag::LuauStoreCSTData2, true};
std::string code = R"(
type Foo = {
read bar: number,
@ -1787,7 +1905,7 @@ TEST_CASE("transpile_type_table_access_modifiers")
TEST_CASE("transpile_type_table_spaces_between_tokens")
{
ScopedFastFlag _{FFlag::LuauStoreCSTData, true};
ScopedFastFlag _{FFlag::LuauStoreCSTData2, true};
std::string code = R"( type Foo = { bar: number, } )";
CHECK_EQ(code, transpile(code, {}, true).code);
@ -1830,7 +1948,7 @@ TEST_CASE("transpile_type_table_spaces_between_tokens")
TEST_CASE("transpile_type_table_preserve_original_indexer_style")
{
ScopedFastFlag _{FFlag::LuauStoreCSTData, true};
ScopedFastFlag _{FFlag::LuauStoreCSTData2, true};
std::string code = R"(
type Foo = {
[number]: string
@ -1846,7 +1964,7 @@ TEST_CASE("transpile_type_table_preserve_original_indexer_style")
TEST_CASE("transpile_type_table_preserve_indexer_location")
{
ScopedFastFlag _{FFlag::LuauStoreCSTData, true};
ScopedFastFlag _{FFlag::LuauStoreCSTData2, true};
std::string code = R"(
type Foo = {
[number]: string,
@ -1875,7 +1993,7 @@ TEST_CASE("transpile_type_table_preserve_indexer_location")
TEST_CASE("transpile_type_table_preserve_property_definition_style")
{
ScopedFastFlag _{FFlag::LuauStoreCSTData, true};
ScopedFastFlag _{FFlag::LuauStoreCSTData2, true};
std::string code = R"(
type Foo = {
["$$typeof1"]: string,
@ -1888,7 +2006,7 @@ TEST_CASE("transpile_type_table_preserve_property_definition_style")
TEST_CASE("transpile_types_preserve_parentheses_style")
{
ScopedFastFlag flags[] = {
{FFlag::LuauStoreCSTData, true},
{FFlag::LuauStoreCSTData2, true},
{FFlag::LuauAstTypeGroup3, true},
};
@ -1929,7 +2047,7 @@ end
TEST_CASE("transpile_type_function_unnamed_arguments")
{
ScopedFastFlag _{FFlag::LuauStoreCSTData, true};
ScopedFastFlag _{FFlag::LuauStoreCSTData2, true};
std::string code = R"( type Foo = () -> () )";
CHECK_EQ(R"( type Foo = () ->() )", transpile(code, {}, true).code);
@ -1963,7 +2081,7 @@ TEST_CASE("transpile_type_function_unnamed_arguments")
TEST_CASE("transpile_type_function_named_arguments")
{
ScopedFastFlag _{FFlag::LuauStoreCSTData, true};
ScopedFastFlag _{FFlag::LuauStoreCSTData2, true};
std::string code = R"( type Foo = (x: string) -> () )";
CHECK_EQ(R"( type Foo = (x: string) ->() )", transpile(code, {}, true).code);
@ -1991,7 +2109,7 @@ TEST_CASE("transpile_type_function_named_arguments")
TEST_CASE("transpile_type_function_generics")
{
ScopedFastFlag _{FFlag::LuauStoreCSTData, true};
ScopedFastFlag _{FFlag::LuauStoreCSTData2, true};
std::string code = R"( type Foo = <X, Y, Z...>() -> () )";
CHECK_EQ(R"( type Foo = <X, Y, Z...>() ->() )", transpile(code, {}, true).code);
@ -2023,4 +2141,55 @@ TEST_CASE("transpile_type_function_generics")
CHECK_EQ(R"( type Foo = <X, Y, Z...> () ->() )", transpile(code, {}, true).code);
}
TEST_CASE("fuzzer_nil_optional")
{
ScopedFastFlag _{FFlag::LuauParseOptionalAsNode2, true};
const std::string code = R"( local x: nil? )";
CHECK_EQ(code, transpile(code, {}, true).code);
}
TEST_CASE("transpile_function_attributes")
{
ScopedFastFlag _{FFlag::LuauStoreCSTData2, true};
std::string code = R"(
@native
function foo()
end
)";
CHECK_EQ(code, transpile(code, {}, true).code);
code = R"(
@native
local function foo()
end
)";
CHECK_EQ(code, transpile(code, {}, true).code);
code = R"(
@checked local function foo()
end
)";
CHECK_EQ(code, transpile(code, {}, true).code);
code = R"(
local foo = @native function() end
)";
CHECK_EQ(code, transpile(code, {}, true).code);
code = R"(
@native
function foo:bar()
end
)";
CHECK_EQ(code, transpile(code, {}, true).code);
code = R"(
@native @checked
function foo:bar()
end
)";
CHECK_EQ(code, transpile(code, {}, true).code);
}
TEST_SUITE_END();

View file

@ -17,7 +17,9 @@ LUAU_FASTFLAG(LuauIndexTypeFunctionImprovements)
LUAU_DYNAMIC_FASTINT(LuauTypeFamilyApplicationCartesianProductLimit)
LUAU_FASTFLAG(LuauIndexTypeFunctionFunctionMetamethods)
LUAU_FASTFLAG(LuauMetatableTypeFunctions)
LUAU_FASTFLAG(LuauMetatablesHaveLength)
LUAU_FASTFLAG(LuauIndexAnyIsAny)
LUAU_FASTFLAG(LuauNewTypeFunReductionChecks2)
struct TypeFunctionFixture : Fixture
{
@ -145,19 +147,17 @@ TEST_CASE_FIXTURE(TypeFunctionFixture, "unsolvable_function")
if (!FFlag::LuauSolverV2)
return;
ScopedFastFlag luauNewTypeFunReductionChecks2{FFlag::LuauNewTypeFunReductionChecks2, true};
CheckResult result = check(R"(
local impossible: <T>(Swap<T>) -> Swap<Swap<T>>
local a = impossible(123)
local b = impossible(true)
)");
LUAU_REQUIRE_ERROR_COUNT(6, result);
CHECK(toString(result.errors[0]) == "Type function instance Swap<Swap<T>> is uninhabited");
CHECK(toString(result.errors[1]) == "Type function instance Swap<T> is uninhabited");
CHECK(toString(result.errors[2]) == "Type function instance Swap<Swap<T>> is uninhabited");
CHECK(toString(result.errors[3]) == "Type function instance Swap<T> is uninhabited");
CHECK(toString(result.errors[4]) == "Type function instance Swap<Swap<T>> is uninhabited");
CHECK(toString(result.errors[5]) == "Type function instance Swap<T> is uninhabited");
LUAU_REQUIRE_ERROR_COUNT(2, result);
CHECK(toString(result.errors[0]) == "Type 'number' could not be converted into 'never'");
CHECK(toString(result.errors[1]) == "Type 'boolean' could not be converted into 'never'");
}
TEST_CASE_FIXTURE(TypeFunctionFixture, "table_internal_functions")
@ -1552,4 +1552,35 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "getmetatable_respects_metatable_metamethod")
CHECK_EQ(toString(requireTypeAlias("Metatable")), "string");
}
TEST_CASE_FIXTURE(BuiltinsFixture, "type_function_correct_cycle_check")
{
if (!FFlag::LuauSolverV2)
return;
ScopedFastFlag luauNewTypeFunReductionChecks2{FFlag::LuauNewTypeFunReductionChecks2, true};
CheckResult result = check(R"(
type foo<T> = { a: add<T, T>, b : add<T, T> }
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(BuiltinsFixture, "len_typefun_on_metatable")
{
ScopedFastFlag newSolver{FFlag::LuauSolverV2, true};
ScopedFastFlag luauMetatablesHaveLength{FFlag::LuauMetatablesHaveLength, true};
CheckResult result = check(R"(
local t = setmetatable({}, { __mode = "v" })
local function f()
table.insert(t, {})
print(#t * 100)
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_SUITE_END();

View file

@ -10,9 +10,9 @@ using namespace Luau;
LUAU_FASTFLAG(LuauSolverV2)
LUAU_FASTFLAG(DebugLuauEqSatSimplification)
LUAU_FASTFLAG(LuauTypeFunReadWriteParents)
LUAU_FASTFLAG(LuauTypeFunPrintFix)
LUAU_FASTFLAG(LuauImproveTypePathsInErrors)
LUAU_FASTFLAG(LuauUserTypeFunTypecheck)
LUAU_FASTFLAG(LuauNewTypeFunReductionChecks2)
TEST_SUITE_BEGIN("UserDefinedTypeFunctionTests");
@ -2030,7 +2030,7 @@ local _:test<number>
TEST_CASE_FIXTURE(BuiltinsFixture, "udtf_print_tab_char_fix")
{
ScopedFastFlag sffs[] = {{FFlag::LuauSolverV2, true}, {FFlag::LuauTypeFunPrintFix, true}};
ScopedFastFlag solverV2{FFlag::LuauSolverV2, true};
CheckResult result = check(R"(
type function test(t)
@ -2103,4 +2103,105 @@ end
LUAU_REQUIRE_ERROR_COUNT(1, result);
}
TEST_CASE_FIXTURE(BuiltinsFixture, "outer_generics_irreducible")
{
ScopedFastFlag newSolver{FFlag::LuauSolverV2, true};
ScopedFastFlag luauNewTypeFunReductionChecks2{FFlag::LuauNewTypeFunReductionChecks2, true};
CheckResult result = check(R"(
type function func(t)
return t
end
type wrap<T> = { a: func<T?> }
local x: wrap<string> = nil :: any
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK(toString(requireType("x"), ToStringOptions{true}) == "{ a: string? }");
}
TEST_CASE_FIXTURE(BuiltinsFixture, "inner_generics_reducible")
{
ScopedFastFlag newSolver{FFlag::LuauSolverV2, true};
CheckResult result = check(R"(
type function func(t)
return t
end
type wrap<T> = { a: func<<T>(T) -> number>, b: T }
local x: wrap<string> = nil :: any
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK(toString(requireType("x"), ToStringOptions{true}) == "{ a: <T>(T) -> number, b: string }");
}
TEST_CASE_FIXTURE(BuiltinsFixture, "blocking_nested_pending_expansions")
{
if (!FFlag::LuauSolverV2)
return;
CheckResult result = check(R"(
type function func(t)
return t
end
type test<T> = { x: T, y: T? }
type wrap<T> = { a: func<(string, keyof<test<T>>) -> number>, b: T }
local x: wrap<string>
local y: keyof<typeof(x)>
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK(toString(requireType("x"), ToStringOptions{true}) == R"({ a: (string, "x" | "y") -> number, b: string })");
CHECK(toString(requireType("y"), ToStringOptions{true}) == R"("a" | "b")");
}
TEST_CASE_FIXTURE(BuiltinsFixture, "blocking_nested_pending_expansions_2")
{
ScopedFastFlag newSolver{FFlag::LuauSolverV2, true};
CheckResult result = check(R"(
type function foo(t)
return types.unionof(t, types.singleton(nil))
end
local x: foo<{a: foo<string>, b: foo<number>}> = nil
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK(toString(requireType("x"), ToStringOptions{true}) == "{ a: string?, b: number? }?");
}
TEST_CASE_FIXTURE(BuiltinsFixture, "irreducible_pending_expansions")
{
if (!FFlag::LuauSolverV2)
return;
ScopedFastFlag luauNewTypeFunReductionChecks2{FFlag::LuauNewTypeFunReductionChecks2, true};
CheckResult result = check(R"(
type function foo(t)
return types.unionof(t, types.singleton(nil))
end
type table<T> = { a: index<T, "a"> }
type wrap<T> = foo<table<T>>
local x: wrap<{a: number}> = { a = 2 }
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK(toString(requireType("x"), ToStringOptions{true}) == "{ a: number }?");
}
TEST_SUITE_END();

View file

@ -12,6 +12,10 @@ using namespace Luau;
LUAU_FASTFLAG(LuauSolverV2)
LUAU_FASTFLAG(LuauFixInfiniteRecursionInNormalization)
LUAU_FASTFLAG(LuauImproveTypePathsInErrors)
LUAU_FASTFLAG(LuauPrecalculateMutatedFreeTypes2)
LUAU_FASTFLAG(LuauDeferBidirectionalInferenceForTableAssignment)
LUAU_FASTFLAG(LuauBidirectionalInferenceUpcast)
LUAU_FASTFLAG(LuauBidirectionalInferenceCollectIndexerTypes)
TEST_SUITE_BEGIN("TypeAliases");
@ -253,8 +257,12 @@ TEST_CASE_FIXTURE(Fixture, "dependent_generic_aliases")
TEST_CASE_FIXTURE(Fixture, "mutually_recursive_generic_aliases")
{
// CLI-116108
DOES_NOT_PASS_NEW_SOLVER_GUARD();
ScopedFastFlag sffs[] = {
{FFlag::LuauPrecalculateMutatedFreeTypes2, true},
{FFlag::LuauDeferBidirectionalInferenceForTableAssignment, true},
{FFlag::LuauBidirectionalInferenceUpcast, true},
{FFlag::LuauBidirectionalInferenceCollectIndexerTypes, true},
};
CheckResult result = check(R"(
--!strict

View file

@ -24,6 +24,7 @@ LUAU_FASTINT(LuauTarjanChildLimit)
LUAU_FASTFLAG(DebugLuauEqSatSimplification)
LUAU_FASTFLAG(LuauUngeneralizedTypesForRecursiveFunctions)
LUAU_FASTFLAG(LuauImproveTypePathsInErrors)
LUAU_FASTFLAG(LuauReduceUnionFollowUnionType)
TEST_SUITE_BEGIN("TypeInferFunctions");
@ -3191,4 +3192,30 @@ TEST_CASE_FIXTURE(Fixture, "recursive_function_calls_should_not_use_the_generali
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "fuzz_unwind_mutually_recursive_union_type_func")
{
ScopedFastFlag sffs[] = {
{FFlag::LuauSolverV2, true},
{FFlag::LuauReduceUnionFollowUnionType, true}
};
// This block ends up minting a type like:
//
// t2 where t1 = union<t2, t1> | union<t2, t1> | union<t2, t1> ; t2 = union<t2, t1>
//
CheckResult result = check(R"(
local _ = ...
function _()
_ = _
end
_[function(...) repeat until _(_[l100]) _ = _ end] += _
)");
LUAU_REQUIRE_ERROR_COUNT(2, result);
auto err0 = get<UnknownSymbol>(result.errors[0]);
CHECK(err0);
CHECK_EQ(err0->name, "l100");
auto err1 = get<NotATable>(result.errors[1]);
CHECK(err1);
}
TEST_SUITE_END();

View file

@ -13,7 +13,7 @@ using namespace Luau;
LUAU_FASTFLAG(LuauSolverV2)
LUAU_FASTFLAG(DebugLuauEqSatSimplification)
LUAU_FASTFLAG(LuauStoreCSTData)
LUAU_FASTFLAG(LuauStoreCSTData2)
LUAU_FASTINT(LuauNormalizeCacheLimit)
LUAU_FASTINT(LuauTarjanChildLimit)
LUAU_FASTINT(LuauTypeInferIterationLimit)
@ -49,7 +49,7 @@ TEST_CASE_FIXTURE(Fixture, "typeguard_inference_incomplete")
end
)";
const std::string expected = FFlag::LuauStoreCSTData ? R"(
const std::string expected = FFlag::LuauStoreCSTData2 ? R"(
function f(a:{fn:()->(a,b...)}): ()
if type(a) == 'boolean' then
local a1:boolean=a
@ -68,7 +68,7 @@ TEST_CASE_FIXTURE(Fixture, "typeguard_inference_incomplete")
end
)";
const std::string expectedWithNewSolver = FFlag::LuauStoreCSTData ? R"(
const std::string expectedWithNewSolver = FFlag::LuauStoreCSTData2 ? R"(
function f(a:{fn:()->(unknown,...unknown)}): ()
if type(a) == 'boolean' then
local a1:{fn:()->(unknown,...unknown)}&boolean=a
@ -87,7 +87,7 @@ TEST_CASE_FIXTURE(Fixture, "typeguard_inference_incomplete")
end
)";
const std::string expectedWithEqSat = FFlag::LuauStoreCSTData ? R"(
const std::string expectedWithEqSat = FFlag::LuauStoreCSTData2 ? R"(
function f(a:{fn:()->(unknown,...unknown)}): ()
if type(a) == 'boolean' then
local a1:{fn:()->(unknown,...unknown)}&boolean=a

View file

@ -14,6 +14,7 @@ LUAU_FASTFLAG(LuauIntersectNotNil)
LUAU_FASTFLAG(LuauSkipNoRefineDuringRefinement)
LUAU_FASTFLAG(LuauFunctionCallsAreNotNilable)
LUAU_FASTFLAG(LuauDoNotLeakNilInRefinement)
LUAU_FASTFLAG(LuauSimplyRefineNotNil)
using namespace Luau;
@ -756,14 +757,24 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "nonoptional_type_can_narrow_to_nil_if_sense_
{
// CLI-115281 Types produced by refinements do not consistently get simplified
CHECK_EQ("(nil & string)?", toString(requireTypeAtPosition({4, 24}))); // type(v) == "nil"
CHECK_EQ(
"(boolean | buffer | class | function | number | string | table | thread) & string", toString(requireTypeAtPosition({6, 24}))
); // type(v) ~= "nil"
if (FFlag::LuauSimplyRefineNotNil)
CHECK_EQ(
"string & ~nil", toString(requireTypeAtPosition({6, 24}))
); // type(v) ~= "nil"
else
CHECK_EQ(
"(boolean | buffer | class | function | number | string | table | thread) & string", toString(requireTypeAtPosition({6, 24}))
); // type(v) ~= "nil"
CHECK_EQ("(nil & string)?", toString(requireTypeAtPosition({10, 24}))); // equivalent to type(v) == "nil"
CHECK_EQ(
"(boolean | buffer | class | function | number | string | table | thread) & string", toString(requireTypeAtPosition({12, 24}))
); // equivalent to type(v) ~= "nil"
if (FFlag::LuauSimplyRefineNotNil)
CHECK_EQ("string & ~nil", toString(requireTypeAtPosition({12, 24}))); // equivalent to type(v) ~= "nil"
else
CHECK_EQ(
"(boolean | buffer | class | function | number | string | table | thread) & string", toString(requireTypeAtPosition({12, 24}))
); // equivalent to type(v) ~= "nil"
}
else
{

View file

@ -7,6 +7,7 @@
using namespace Luau;
LUAU_FASTFLAG(LuauSolverV2)
LUAU_FASTFLAG(LuauPropagateExpectedTypesForCalls)
LUAU_FASTFLAG(LuauImproveTypePathsInErrors)
TEST_SUITE_BEGIN("TypeSingletons");
@ -152,6 +153,26 @@ TEST_CASE_FIXTURE(Fixture, "overloaded_function_call_with_singletons")
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "overloaded_function_resolution_singleton_parameters")
{
ScopedFastFlag sff{FFlag::LuauPropagateExpectedTypesForCalls, true};
CheckResult result = check(R"(
type A = ("A") -> string
type B = ("B") -> number
local function foo(f: A & B)
return f("A"), f("B")
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
TypeId t = requireType("foo");
const FunctionType* fooType = get<FunctionType>(requireType("foo"));
REQUIRE(fooType != nullptr);
CHECK(toString(t) == "(((\"A\") -> string) & ((\"B\") -> number)) -> (string, number)");
}
TEST_CASE_FIXTURE(Fixture, "overloaded_function_call_with_singletons_mismatch")
{
DOES_NOT_PASS_NEW_SOLVER_GUARD();

View file

@ -30,6 +30,8 @@ LUAU_FASTFLAG(LuauBidirectionalInferenceUpcast)
LUAU_FASTFLAG(DebugLuauAssertOnForcedConstraint)
LUAU_FASTFLAG(LuauSearchForRefineableType)
LUAU_FASTFLAG(LuauImproveTypePathsInErrors)
LUAU_FASTFLAG(LuauBidirectionalInferenceCollectIndexerTypes)
LUAU_FASTFLAG(LuauBidirectionalFailsafe)
TEST_SUITE_BEGIN("TableTests");
@ -5168,34 +5170,35 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "metatable_union_type")
TEST_CASE_FIXTURE(Fixture, "function_check_constraint_too_eager")
{
// NOTE: All of these examples should have no errors, but
// bidirectional inference is known to be broken.
ScopedFastFlag sffs[] = {
{FFlag::LuauSolverV2, true},
{FFlag::LuauPrecalculateMutatedFreeTypes2, true},
{FFlag::LuauDeferBidirectionalInferenceForTableAssignment, true},
{FFlag::LuauBidirectionalInferenceCollectIndexerTypes, true},
};
auto result = check(R"(
CheckResult result = check(R"(
local function doTheThing(_: { [string]: unknown }) end
doTheThing({
['foo'] = 5,
['bar'] = 'heyo',
})
)");
LUAU_CHECK_ERROR_COUNT(1, result);
LUAU_CHECK_NO_ERROR(result, ConstraintSolvingIncompleteError);
LUAU_CHECK_ERROR_COUNT(1, check(R"(
LUAU_REQUIRE_NO_ERRORS(result);
result = check(R"(
type Input = { [string]: unknown }
local i : Input = {
[('%s'):format('3.14')]=5,
['stringField']='Heyo'
}
)"));
)");
LUAU_REQUIRE_NO_ERRORS(result);
// This example previously asserted due to eagerly mutating the underlying
// table type.
result = check(R"(
type Input = { [string]: unknown }
@ -5206,8 +5209,45 @@ TEST_CASE_FIXTURE(Fixture, "function_check_constraint_too_eager")
['stringField']='Heyo'
})
)");
LUAU_CHECK_ERROR_COUNT(1, result);
LUAU_CHECK_NO_ERROR(result, ConstraintSolvingIncompleteError);
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(BuiltinsFixture, "magic_functions_bidirectionally_inferred")
{
ScopedFastFlag sffs[] = {
{FFlag::LuauSolverV2, true},
{FFlag::LuauPrecalculateMutatedFreeTypes2, true},
{FFlag::LuauDeferBidirectionalInferenceForTableAssignment, true},
{FFlag::LuauBidirectionalInferenceCollectIndexerTypes, true},
};
CheckResult result = check(R"(
local function getStuff(): (string, number, string)
return "hello", 42, "world"
end
local t: { [string]: number } = {
[select(1, getStuff())] = select(2, getStuff()),
[select(3, getStuff())] = select(2, getStuff())
}
)");
LUAU_REQUIRE_NO_ERRORS(result);
result = check(R"(
local function getStuff(): (string, number, string)
return "hello", 42, "world"
end
local t: { [string]: number } = {
[select(1, getStuff())] = select(2, getStuff()),
[select(3, getStuff())] = select(3, getStuff())
}
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
auto err = get<TypeMismatch>(result.errors[0]);
CHECK_EQ("{ [string]: number | string }", toString(err->givenType));
CHECK_EQ("{ [string]: number }", toString(err->wantedType));
}
@ -5471,6 +5511,8 @@ TEST_CASE_FIXTURE(Fixture, "missing_fields_bidirectional_inference")
{FFlag::LuauSolverV2, true},
{FFlag::LuauPrecalculateMutatedFreeTypes2, true},
{FFlag::LuauDeferBidirectionalInferenceForTableAssignment, true},
{FFlag::LuauBidirectionalInferenceUpcast, true},
{FFlag::LuauBidirectionalInferenceCollectIndexerTypes, true},
};
auto result = check(R"(
@ -5478,7 +5520,8 @@ TEST_CASE_FIXTURE(Fixture, "missing_fields_bidirectional_inference")
local b: Book = { title = "The Odyssey" }
local t: { Book } = {
{ title = "The Illiad", author = "Homer" },
{ author = "Virgil" }
{ title = "Inferno", author = "Virgil" },
{ author = "Virgil" },
}
)");
@ -5490,12 +5533,49 @@ TEST_CASE_FIXTURE(Fixture, "missing_fields_bidirectional_inference")
CHECK_EQ(result.errors[0].location, Location{{2, 24}, {2, 49}});
err = get<TypeMismatch>(result.errors[1]);
REQUIRE(err);
CHECK_EQ(toString(err->givenType), "{{ author: string } | { author: string, title: string }}");
// CLI-144203: This could be better.
CHECK_EQ(toString(err->givenType), "{{ author: string }}");
CHECK_EQ(toString(err->wantedType), "{Book}");
CHECK_EQ(result.errors[1].location, Location{{3, 28}, {6, 9}});
CHECK_EQ(result.errors[1].location, Location{{3, 28}, {7, 9}});
}
TEST_CASE_FIXTURE(Fixture, "generic_index_syntax_bidirectional_infer_with_tables")
{
ScopedFastFlag sffs[] = {
{FFlag::LuauSolverV2, true},
{FFlag::LuauPrecalculateMutatedFreeTypes2, true},
{FFlag::LuauDeferBidirectionalInferenceForTableAssignment, true},
{FFlag::LuauBidirectionalInferenceUpcast, true},
{FFlag::LuauBidirectionalInferenceCollectIndexerTypes, true},
};
auto result = check((R"(
local function getStatus(): string
return "Yeah can you look in returned books?"
end
local function getPratchettStatus()
return { isLate = true }
end
type Status = { isLate: boolean, daysLate: number? }
local key1 = "Great Expecations"
local key2 = "The Outsiders"
local key3 = "Guards! Guards!"
local books: { [string]: Status } = {
[key1] = { isLate = true, daysLate = "coconut" },
[key2] = getStatus(),
[key3] = getPratchettStatus()
}
)"));
LUAU_CHECK_ERROR_COUNT(1, result);
auto err = get<TypeMismatch>(result.errors[0]);
REQUIRE(err);
// NOTE: This is because the inferred keys of `books` are all primitive types.
CHECK_EQ(toString(err->givenType), "{ [string | string | string]: string | { daysLate: string, isLate: boolean } | { isLate: boolean } }");
CHECK_EQ(toString(err->wantedType), "{ [string]: Status }");
}
TEST_CASE_FIXTURE(Fixture, "deeply_nested_classish_inference")
{
ScopedFastFlag sffs[] = {
@ -5572,5 +5652,43 @@ TEST_CASE_FIXTURE(Fixture, "bigger_nested_table_causes_big_type_error")
CHECK_EQ(expected, toString(result.errors[0]));
}
TEST_CASE_FIXTURE(Fixture, "unsafe_bidirectional_mutation")
{
ScopedFastFlag _{FFlag::LuauBidirectionalFailsafe, true};
// It's kind of suspect that we allow multiple definitions of keys in
// a single table.
LUAU_REQUIRE_NO_ERRORS(check(R"(
type F = {
_G: () -> ()
}
function _()
return
end
local function h(f: F) end
h({
_G = {},
_G = _,
})
)"));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "function_call_in_indexer_with_compound_assign")
{
ScopedFastFlag _{FFlag::LuauBidirectionalFailsafe, true};
// This has a bunch of errors, we really just need it to not crash / assert.
std::ignore = check(R"(
--!strict
local _ = 7143424
_[
setfenv(
...,
{
n0 = _,
}
)
] *= _
)");
}
TEST_SUITE_END();

View file

@ -30,6 +30,7 @@ LUAU_FASTFLAG(LuauInferLocalTypesInMultipleAssignments)
LUAU_FASTFLAG(LuauUnifyMetatableWithAny)
LUAU_FASTFLAG(LuauExtraFollows)
LUAU_FASTFLAG(LuauImproveTypePathsInErrors)
LUAU_FASTFLAG(LuauTypeCheckerAcceptNumberConcats)
using namespace Luau;
@ -1862,7 +1863,6 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "getmetatable_works_with_any")
end,
}
)"));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "getmetatable_infer_any_ret")
@ -1937,14 +1937,14 @@ end
TEST_CASE_FIXTURE(Fixture, "concat_string_with_string_union")
{
ScopedFastFlag _{FFlag::LuauSolverV2, true};
ScopedFastFlag fixNumberConcats{FFlag::LuauTypeCheckerAcceptNumberConcats, true};
LUAU_REQUIRE_NO_ERRORS(check(R"(
local function foo(n : number): string return "" end
local function bar(n: number, m: string) end
local function concat_stuff(x, y)
local z = foo(x)
bar(y, z)
local function concat_stuff(x: string, y : string | number)
return x .. y
end
)"));
}
TEST_SUITE_END();