Merge remote-tracking branch 'upstream/master' into string-interpolation

This commit is contained in:
Kampfkarren 2022-08-22 14:55:09 -07:00
commit 1c172d2884
88 changed files with 4037 additions and 1267 deletions

View file

@ -13,12 +13,9 @@ on:
jobs:
callgrind:
name: callgrind ${{ matrix.compiler }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-22.04]
compiler: [g++]
benchResultsRepo:
- { name: "luau-lang/benchmark-data", branch: "main" }
@ -31,8 +28,19 @@ jobs:
run: |
sudo apt-get install valgrind
- name: Build Luau
run: CXX=${{ matrix.compiler }} make config=release CALLGRIND=1 luau luau-analyze
- name: Build Luau (gcc)
run: |
CXX=g++ make config=profile luau
cp luau luau-gcc
- name: Build Luau (clang)
run: |
make config=profile clean
CXX=clang++ make config=profile luau luau-analyze
- name: Run benchmark (bench-gcc)
run: |
python bench/bench.py --callgrind --vm "./luau-gcc -O2" | tee -a bench-gcc-output.txt
- name: Run benchmark (bench)
run: |
@ -71,11 +79,19 @@ jobs:
- name: Store results (bench)
uses: Roblox/rhysd-github-action-benchmark@v-luau
with:
name: callgrind ${{ matrix.compiler }}
name: callgrind clang
tool: "benchmarkluau"
output-file-path: ./bench-output.txt
external-data-json-path: ./gh-pages/bench.json
- name: Store results (bench-gcc)
uses: Roblox/rhysd-github-action-benchmark@v-luau
with:
name: callgrind gcc
tool: "benchmarkluau"
output-file-path: ./bench-gcc-output.txt
external-data-json-path: ./gh-pages/bench-gcc.json
- name: Store results (analyze)
uses: Roblox/rhysd-github-action-benchmark@v-luau
with:

View file

@ -0,0 +1,38 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#pragma once
#include "Luau/NotNull.h"
#include "Luau/Substitution.h"
#include "Luau/TypeVar.h"
#include <memory>
namespace Luau
{
struct TypeArena;
struct Scope;
struct InternalErrorReporter;
using ScopePtr = std::shared_ptr<Scope>;
// A substitution which replaces free types by any
struct Anyification : Substitution
{
Anyification(TypeArena* arena, const ScopePtr& scope, InternalErrorReporter* iceHandler, TypeId anyType, TypePackId anyTypePack);
NotNull<Scope> scope;
InternalErrorReporter* iceHandler;
TypeId anyType;
TypePackId anyTypePack;
bool normalizationTooComplex = false;
bool isDirty(TypeId ty) override;
bool isDirty(TypePackId tp) override;
TypeId clean(TypeId ty) override;
TypePackId clean(TypePackId tp) override;
bool ignoreChildren(TypeId ty) override;
bool ignoreChildren(TypePackId ty) override;
};
}

View file

@ -19,6 +19,17 @@ struct TypeChecker;
using ModulePtr = std::shared_ptr<Module>;
enum class AutocompleteContext
{
Unknown,
Expression,
Statement,
Property,
Type,
Keyword,
String,
};
enum class AutocompleteEntryKind
{
Property,
@ -66,11 +77,13 @@ struct AutocompleteResult
{
AutocompleteEntryMap entryMap;
std::vector<AstNode*> ancestry;
AutocompleteContext context = AutocompleteContext::Unknown;
AutocompleteResult() = default;
AutocompleteResult(AutocompleteEntryMap entryMap, std::vector<AstNode*> ancestry)
AutocompleteResult(AutocompleteEntryMap entryMap, std::vector<AstNode*> ancestry, AutocompleteContext context)
: entryMap(std::move(entryMap))
, ancestry(std::move(ancestry))
, context(context)
{
}
};

View file

@ -25,6 +25,6 @@ TypePackId clone(TypePackId tp, TypeArena& dest, CloneState& cloneState);
TypeId clone(TypeId tp, TypeArena& dest, CloneState& cloneState);
TypeFun clone(const TypeFun& typeFun, TypeArena& dest, CloneState& cloneState);
TypeId shallowClone(TypeId ty, TypeArena& dest, const TxnLog* log);
TypeId shallowClone(TypeId ty, TypeArena& dest, const TxnLog* log, bool alwaysClone = false);
} // namespace Luau

View file

@ -40,7 +40,6 @@ struct GeneralizationConstraint
{
TypeId generalizedType;
TypeId sourceType;
Scope* scope;
};
// subType ~ inst superType
@ -85,13 +84,14 @@ using ConstraintPtr = std::unique_ptr<struct Constraint>;
struct Constraint
{
explicit Constraint(ConstraintV&& c);
Constraint(ConstraintV&& c, NotNull<Scope> scope);
Constraint(const Constraint&) = delete;
Constraint& operator=(const Constraint&) = delete;
ConstraintV c;
std::vector<NotNull<Constraint>> dependencies;
NotNull<Scope> scope;
};
inline Constraint& asMutable(const Constraint& c)

View file

@ -28,6 +28,7 @@ struct ConstraintGraphBuilder
std::vector<std::pair<Location, ScopePtr>> scopes;
ModuleName moduleName;
ModulePtr module;
SingletonTypes& singletonTypes;
const NotNull<TypeArena> arena;
// The root scope of the module we're generating constraints for.
@ -53,9 +54,9 @@ struct ConstraintGraphBuilder
// Occasionally constraint generation needs to produce an ICE.
const NotNull<InternalErrorReporter> ice;
NotNull<Scope> globalScope;
ScopePtr globalScope;
ConstraintGraphBuilder(const ModuleName& moduleName, TypeArena* arena, NotNull<InternalErrorReporter> ice, NotNull<Scope> globalScope);
ConstraintGraphBuilder(const ModuleName& moduleName, ModulePtr module, TypeArena* arena, NotNull<InternalErrorReporter> ice, const ScopePtr& globalScope);
/**
* Fabricates a new free type belonging to a given scope.
@ -71,10 +72,10 @@ struct ConstraintGraphBuilder
/**
* Fabricates a scope that is a child of another scope.
* @param location the lexical extent of the scope in the source code.
* @param node the lexical node that the scope belongs to.
* @param parent the parent scope of the new scope. Must not be null.
*/
ScopePtr childScope(Location location, const ScopePtr& parent);
ScopePtr childScope(AstNode* node, const ScopePtr& parent);
/**
* Adds a new constraint with no dependencies to a given scope.
@ -103,10 +104,13 @@ struct ConstraintGraphBuilder
void visit(const ScopePtr& scope, AstStatBlock* block);
void visit(const ScopePtr& scope, AstStatLocal* local);
void visit(const ScopePtr& scope, AstStatFor* for_);
void visit(const ScopePtr& scope, AstStatWhile* while_);
void visit(const ScopePtr& scope, AstStatRepeat* repeat);
void visit(const ScopePtr& scope, AstStatLocalFunction* function);
void visit(const ScopePtr& scope, AstStatFunction* function);
void visit(const ScopePtr& scope, AstStatReturn* ret);
void visit(const ScopePtr& scope, AstStatAssign* assign);
void visit(const ScopePtr& scope, AstStatCompoundAssign* assign);
void visit(const ScopePtr& scope, AstStatIf* ifStatement);
void visit(const ScopePtr& scope, AstStatTypeAlias* alias);
void visit(const ScopePtr& scope, AstStatDeclareGlobal* declareGlobal);
@ -131,6 +135,8 @@ struct ConstraintGraphBuilder
TypeId check(const ScopePtr& scope, AstExprIndexExpr* indexExpr);
TypeId check(const ScopePtr& scope, AstExprUnary* unary);
TypeId check(const ScopePtr& scope, AstExprBinary* binary);
TypeId check(const ScopePtr& scope, AstExprIfElse* ifElse);
TypeId check(const ScopePtr& scope, AstExprTypeAssertion* typeAssert);
struct FunctionSignature
{

View file

@ -60,6 +60,9 @@ struct ConstraintSolver
// Memoized instantiations of type aliases.
DenseHashMap<InstantiationSignature, TypeId, HashInstantiationSignature> instantiatedAliases{{}};
// Recorded errors that take place within the solver.
ErrorVec errors;
ConstraintSolverLogger logger;
explicit ConstraintSolver(TypeArena* arena, NotNull<Scope> rootScope);
@ -115,7 +118,7 @@ struct ConstraintSolver
* @param subType the sub-type to unify.
* @param superType the super-type to unify.
*/
void unify(TypeId subType, TypeId superType);
void unify(TypeId subType, TypeId superType, NotNull<Scope> scope);
/**
* Creates a new Unifier and performs a single unification operation. Commits
@ -123,13 +126,15 @@ struct ConstraintSolver
* @param subPack the sub-type pack to unify.
* @param superPack the super-type pack to unify.
*/
void unify(TypePackId subPack, TypePackId superPack);
void unify(TypePackId subPack, TypePackId superPack, NotNull<Scope> scope);
/** Pushes a new solver constraint to the solver.
* @param cv the body of the constraint.
**/
void pushConstraint(ConstraintV cv);
void pushConstraint(ConstraintV cv, NotNull<Scope> scope);
void reportError(TypeErrorData&& data, const Location& location);
void reportError(TypeError e);
private:
/**
* Marks a constraint as being blocked on a type or type pack. The constraint

View file

@ -154,7 +154,7 @@ struct Frontend
LoadDefinitionFileResult loadDefinitionFile(std::string_view source, const std::string& packageName);
NotNull<Scope> getGlobalScope();
ScopePtr getGlobalScope();
private:
ModulePtr check(const SourceModule& sourceModule, Mode mode, const ScopePtr& environmentScope);

View file

@ -53,6 +53,7 @@ struct LintWarning
Code_MisleadingAndOr = 25,
Code_CommentDirective = 26,
Code_IntegerParsing = 27,
Code_ComparisonPrecedence = 28,
Code__Count
};

View file

@ -77,6 +77,8 @@ struct Module
DenseHashMap<const AstExpr*, TypeId> astOverloadResolvedTypes{nullptr};
DenseHashMap<const AstType*, TypeId> astResolvedTypes{nullptr};
DenseHashMap<const AstTypePack*, TypePackId> astResolvedTypePacks{nullptr};
// Map AST nodes to the scope they create. Cannot be NotNull<Scope> because we need a sentinel value for the map.
DenseHashMap<const AstNode*, Scope*> astScopes{nullptr};
std::unordered_map<Name, TypeId> declaredGlobals;
ErrorVec errors;

View file

@ -1,20 +1,28 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#include "Luau/Substitution.h"
#include "Luau/TypeVar.h"
#include "Luau/Module.h"
#include "Luau/NotNull.h"
#include "Luau/TypeVar.h"
#include <memory>
namespace Luau
{
struct InternalErrorReporter;
struct Module;
struct Scope;
bool isSubtype(TypeId subTy, TypeId superTy, InternalErrorReporter& ice);
bool isSubtype(TypePackId subTy, TypePackId superTy, InternalErrorReporter& ice);
using ModulePtr = std::shared_ptr<Module>;
std::pair<TypeId, bool> normalize(TypeId ty, TypeArena& arena, InternalErrorReporter& ice);
bool isSubtype(TypeId subTy, TypeId superTy, NotNull<Scope> scope, InternalErrorReporter& ice);
bool isSubtype(TypePackId subTy, TypePackId superTy, NotNull<Scope> scope, InternalErrorReporter& ice);
std::pair<TypeId, bool> normalize(TypeId ty, NotNull<Scope> scope, TypeArena& arena, InternalErrorReporter& ice);
std::pair<TypeId, bool> normalize(TypeId ty, NotNull<Module> module, InternalErrorReporter& ice);
std::pair<TypeId, bool> normalize(TypeId ty, const ModulePtr& module, InternalErrorReporter& ice);
std::pair<TypePackId, bool> normalize(TypePackId ty, TypeArena& arena, InternalErrorReporter& ice);
std::pair<TypePackId, bool> normalize(TypePackId ty, NotNull<Scope> scope, TypeArena& arena, InternalErrorReporter& ice);
std::pair<TypePackId, bool> normalize(TypePackId ty, NotNull<Module> module, InternalErrorReporter& ice);
std::pair<TypePackId, bool> normalize(TypePackId ty, const ModulePtr& module, InternalErrorReporter& ice);
} // namespace Luau

View file

@ -36,8 +36,6 @@ struct Scope
// All the children of this scope.
std::vector<NotNull<Scope>> children;
std::unordered_map<Symbol, Binding> bindings;
std::unordered_map<Name, TypeFun> typeBindings;
std::unordered_map<Name, TypePackId> typePackBindings;
TypePackId returnType;
std::optional<TypePackId> varargPack;
// All constraints belonging to this scope.
@ -52,8 +50,6 @@ struct Scope
std::unordered_map<Name, std::unordered_map<Name, TypeFun>> importedTypeBindings;
std::optional<TypeId> lookup(Symbol sym);
std::optional<TypeFun> lookupTypeBinding(const Name& name);
std::optional<TypePackId> lookupTypePackBinding(const Name& name);
std::optional<TypeFun> lookupType(const Name& name);
std::optional<TypeFun> lookupImportedType(const Name& moduleAlias, const Name& name);

View file

@ -1,6 +1,7 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#pragma once
#include "Luau/Anyification.h"
#include "Luau/Predicate.h"
#include "Luau/Error.h"
#include "Luau/Module.h"
@ -16,6 +17,8 @@
#include <unordered_map>
#include <unordered_set>
LUAU_FASTFLAG(LuauClassTypeVarsInSubstitution)
namespace Luau
{
@ -34,37 +37,6 @@ const AstStat* getFallthrough(const AstStat* node);
struct UnifierOptions;
struct Unifier;
// A substitution which replaces free types by any
struct Anyification : Substitution
{
Anyification(TypeArena* arena, InternalErrorReporter* iceHandler, TypeId anyType, TypePackId anyTypePack)
: Substitution(TxnLog::empty(), arena)
, iceHandler(iceHandler)
, anyType(anyType)
, anyTypePack(anyTypePack)
{
}
InternalErrorReporter* iceHandler;
TypeId anyType;
TypePackId anyTypePack;
bool normalizationTooComplex = false;
bool isDirty(TypeId ty) override;
bool isDirty(TypePackId tp) override;
TypeId clean(TypeId ty) override;
TypePackId clean(TypePackId tp) override;
bool ignoreChildren(TypeId ty) override
{
return ty->persistent;
}
bool ignoreChildren(TypePackId ty) override
{
return ty->persistent;
}
};
struct GenericTypeDefinitions
{
std::vector<GenericTypeDefinition> genericTypes;
@ -192,32 +164,32 @@ struct TypeChecker
/** Attempt to unify the types.
* Treat any failures as type errors in the final typecheck report.
*/
bool unify(TypeId subTy, TypeId superTy, const Location& location);
bool unify(TypeId subTy, TypeId superTy, const Location& location, const UnifierOptions& options);
bool unify(TypePackId subTy, TypePackId superTy, const Location& location, CountMismatch::Context ctx = CountMismatch::Context::Arg);
bool unify(TypeId subTy, TypeId superTy, const ScopePtr& scope, const Location& location);
bool unify(TypeId subTy, TypeId superTy, const ScopePtr& scope, const Location& location, const UnifierOptions& options);
bool unify(TypePackId subTy, TypePackId superTy, const ScopePtr& scope, const Location& location, CountMismatch::Context ctx = CountMismatch::Context::Arg);
/** Attempt to unify the types.
* If this fails, and the subTy type can be instantiated, do so and try unification again.
*/
bool unifyWithInstantiationIfNeeded(const ScopePtr& scope, TypeId subTy, TypeId superTy, const Location& location);
void unifyWithInstantiationIfNeeded(const ScopePtr& scope, TypeId subTy, TypeId superTy, Unifier& state);
bool unifyWithInstantiationIfNeeded(TypeId subTy, TypeId superTy, const ScopePtr& scope, const Location& location);
void unifyWithInstantiationIfNeeded(TypeId subTy, TypeId superTy, const ScopePtr& scope, Unifier& state);
/** Attempt to unify.
* If there are errors, undo everything and return the errors.
* If there are no errors, commit and return an empty error vector.
*/
template<typename Id>
ErrorVec tryUnify_(Id subTy, Id superTy, const Location& location);
ErrorVec tryUnify(TypeId subTy, TypeId superTy, const Location& location);
ErrorVec tryUnify(TypePackId subTy, TypePackId superTy, const Location& location);
ErrorVec tryUnify_(Id subTy, Id superTy, const ScopePtr& scope, const Location& location);
ErrorVec tryUnify(TypeId subTy, TypeId superTy, const ScopePtr& scope, const Location& location);
ErrorVec tryUnify(TypePackId subTy, TypePackId superTy, const ScopePtr& scope, const Location& location);
// Test whether the two type vars unify. Never commits the result.
template<typename Id>
ErrorVec canUnify_(Id subTy, Id superTy, const Location& location);
ErrorVec canUnify(TypeId subTy, TypeId superTy, const Location& location);
ErrorVec canUnify(TypePackId subTy, TypePackId superTy, const Location& location);
ErrorVec canUnify_(Id subTy, Id superTy, const ScopePtr& scope, const Location& location);
ErrorVec canUnify(TypeId subTy, TypeId superTy, const ScopePtr& scope, const Location& location);
ErrorVec canUnify(TypePackId subTy, TypePackId superTy, const ScopePtr& scope, const Location& location);
void unifyLowerBound(TypePackId subTy, TypePackId superTy, TypeLevel demotedLevel, const Location& location);
void unifyLowerBound(TypePackId subTy, TypePackId superTy, TypeLevel demotedLevel, const ScopePtr& scope, const Location& location);
std::optional<TypeId> findMetatableEntry(TypeId type, std::string entry, const Location& location, bool addErrors);
std::optional<TypeId> findTablePropertyRespectingMeta(TypeId lhsType, Name name, const Location& location, bool addErrors);
@ -286,7 +258,7 @@ private:
void reportErrorCodeTooComplex(const Location& location);
private:
Unifier mkUnifier(const Location& location);
Unifier mkUnifier(const ScopePtr& scope, const Location& location);
// These functions are only safe to call when we are in the process of typechecking a module.
@ -308,7 +280,7 @@ public:
std::pair<std::optional<TypeId>, bool> pickTypesFromSense(TypeId type, bool sense);
private:
TypeId unionOfTypes(TypeId a, TypeId b, const Location& location, bool unifyFreeTypes = true);
TypeId unionOfTypes(TypeId a, TypeId b, const ScopePtr& scope, const Location& location, bool unifyFreeTypes = true);
// ex
// TypeId id = addType(FreeTypeVar());

View file

@ -13,7 +13,10 @@ namespace Luau
using ScopePtr = std::shared_ptr<struct Scope>;
std::optional<TypeId> findMetatableEntry(ErrorVec& errors, TypeId type, std::string entry, Location location);
std::optional<TypeId> findTablePropertyRespectingMeta(ErrorVec& errors, TypeId ty, Name name, Location location);
std::optional<TypeId> findMetatableEntry(ErrorVec& errors, TypeId type, const std::string& entry, Location location);
std::optional<TypeId> findTablePropertyRespectingMeta(ErrorVec& errors, TypeId ty, const std::string& name, Location location);
std::optional<TypeId> getIndexTypeFromType(
const ScopePtr& scope, ErrorVec& errors, TypeArena* arena, TypeId type, const std::string& prop, const Location& location, bool addErrors,
InternalErrorReporter& handle);
} // namespace Luau

View file

@ -717,6 +717,7 @@ struct TypeIterator
stack.push_front({t, 0});
seen.insert(t);
descend();
}
TypeIterator<T>& operator++()
@ -748,17 +749,19 @@ struct TypeIterator
const TypeId& operator*()
{
LUAU_ASSERT(!stack.empty());
descend();
LUAU_ASSERT(!stack.empty());
auto [t, currentIndex] = stack.front();
LUAU_ASSERT(t);
const std::vector<TypeId>& types = getTypes(t);
LUAU_ASSERT(currentIndex < types.size());
const TypeId& ty = types[currentIndex];
LUAU_ASSERT(!get<T>(follow(ty)));
return ty;
}

View file

@ -10,7 +10,7 @@ namespace Luau
{
void* pagedAllocate(size_t size);
void pagedDeallocate(void* ptr);
void pagedDeallocate(void* ptr, size_t size);
void pagedFreeze(void* ptr, size_t size);
void pagedUnfreeze(void* ptr, size_t size);
@ -113,7 +113,7 @@ private:
for (size_t i = 0; i < blockSize; ++i)
block[i].~T();
pagedDeallocate(block);
pagedDeallocate(block, kBlockSizeBytes);
}
stuff.clear();

View file

@ -3,9 +3,10 @@
#include "Luau/Error.h"
#include "Luau/Location.h"
#include "Luau/Scope.h"
#include "Luau/TxnLog.h"
#include "Luau/TypeInfer.h"
#include "Luau/TypeArena.h"
#include "Luau/TypeInfer.h"
#include "Luau/UnifierSharedState.h"
#include <unordered_set>
@ -48,6 +49,7 @@ struct Unifier
TypeArena* const types;
Mode mode;
NotNull<Scope> scope; // const Scope maybe
TxnLog log;
ErrorVec errors;
Location location;
@ -57,7 +59,7 @@ struct Unifier
UnifierSharedState& sharedState;
Unifier(TypeArena* types, Mode mode, const Location& location, Variance variance, UnifierSharedState& sharedState, TxnLog* parentLog = nullptr);
Unifier(TypeArena* types, Mode mode, NotNull<Scope> scope, const Location& location, Variance variance, UnifierSharedState& sharedState, TxnLog* parentLog = nullptr);
// Test whether the two type vars unify. Never commits the result.
ErrorVec canUnify(TypeId subTy, TypeId superTy);
@ -109,11 +111,11 @@ private:
public:
void unifyLowerBound(TypePackId subTy, TypePackId superTy, TypeLevel demotedLevel);
// Report an "infinite type error" if the type "needle" already occurs within "haystack"
void occursCheck(TypeId needle, TypeId haystack);
void occursCheck(DenseHashSet<TypeId>& seen, TypeId needle, TypeId haystack);
void occursCheck(TypePackId needle, TypePackId haystack);
void occursCheck(DenseHashSet<TypePackId>& seen, TypePackId needle, TypePackId haystack);
// Returns true if the type "needle" already occurs within "haystack" and reports an "infinite type error"
bool occursCheck(TypeId needle, TypeId haystack);
bool occursCheck(DenseHashSet<TypeId>& seen, TypeId needle, TypeId haystack);
bool occursCheck(TypePackId needle, TypePackId haystack);
bool occursCheck(DenseHashSet<TypePackId>& seen, TypePackId needle, TypePackId haystack);
Unifier makeChildUnifier();

View file

@ -69,12 +69,14 @@ struct GenericTypeVarVisitor
using Set = S;
Set seen;
bool skipBoundTypes = false;
int recursionCounter = 0;
GenericTypeVarVisitor() = default;
explicit GenericTypeVarVisitor(Set seen)
explicit GenericTypeVarVisitor(Set seen, bool skipBoundTypes = false)
: seen(std::move(seen))
, skipBoundTypes(skipBoundTypes)
{
}
@ -199,7 +201,9 @@ struct GenericTypeVarVisitor
if (auto btv = get<BoundTypeVar>(ty))
{
if (visit(ty, *btv))
if (skipBoundTypes)
traverse(btv->boundTo);
else if (visit(ty, *btv))
traverse(btv->boundTo);
}
else if (auto ftv = get<FreeTypeVar>(ty))
@ -229,7 +233,11 @@ struct GenericTypeVarVisitor
else if (auto ttv = get<TableTypeVar>(ty))
{
// Some visitors want to see bound tables, that's why we traverse the original type
if (visit(ty, *ttv))
if (skipBoundTypes && ttv->boundTo)
{
traverse(*ttv->boundTo);
}
else if (visit(ty, *ttv))
{
if (ttv->boundTo)
{
@ -394,13 +402,17 @@ struct GenericTypeVarVisitor
*/
struct TypeVarVisitor : GenericTypeVarVisitor<std::unordered_set<void*>>
{
explicit TypeVarVisitor(bool skipBoundTypes = false)
: GenericTypeVarVisitor{{}, skipBoundTypes}
{
}
};
/// Visit each type under a given type. Each type will only be checked once even if there are multiple paths to it.
struct TypeVarOnceVisitor : GenericTypeVarVisitor<DenseHashSet<void*>>
{
TypeVarOnceVisitor()
: GenericTypeVarVisitor{DenseHashSet<void*>{nullptr}}
explicit TypeVarOnceVisitor(bool skipBoundTypes = false)
: GenericTypeVarVisitor{DenseHashSet<void*>{nullptr}, skipBoundTypes}
{
}
};

View file

@ -0,0 +1,96 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#include "Luau/Anyification.h"
#include "Luau/Common.h"
#include "Luau/Normalize.h"
#include "Luau/TxnLog.h"
LUAU_FASTFLAG(LuauClassTypeVarsInSubstitution)
namespace Luau
{
Anyification::Anyification(TypeArena* arena, const ScopePtr& scope, InternalErrorReporter* iceHandler, TypeId anyType, TypePackId anyTypePack)
: Substitution(TxnLog::empty(), arena)
, scope(NotNull{scope.get()})
, iceHandler(iceHandler)
, anyType(anyType)
, anyTypePack(anyTypePack)
{
}
bool Anyification::isDirty(TypeId ty)
{
if (ty->persistent)
return false;
if (const TableTypeVar* ttv = log->getMutable<TableTypeVar>(ty))
return (ttv->state == TableState::Free || ttv->state == TableState::Unsealed);
else if (log->getMutable<FreeTypeVar>(ty))
return true;
else if (get<ConstrainedTypeVar>(ty))
return true;
else
return false;
}
bool Anyification::isDirty(TypePackId tp)
{
if (tp->persistent)
return false;
if (log->getMutable<FreeTypePack>(tp))
return true;
else
return false;
}
TypeId Anyification::clean(TypeId ty)
{
LUAU_ASSERT(isDirty(ty));
if (const TableTypeVar* ttv = log->getMutable<TableTypeVar>(ty))
{
TableTypeVar clone = TableTypeVar{ttv->props, ttv->indexer, ttv->level, TableState::Sealed};
clone.definitionModuleName = ttv->definitionModuleName;
clone.name = ttv->name;
clone.syntheticName = ttv->syntheticName;
clone.tags = ttv->tags;
TypeId res = addType(std::move(clone));
asMutable(res)->normal = ty->normal;
return res;
}
else if (auto ctv = get<ConstrainedTypeVar>(ty))
{
std::vector<TypeId> copy = ctv->parts;
for (TypeId& ty : copy)
ty = replace(ty);
TypeId res = copy.size() == 1 ? copy[0] : addType(UnionTypeVar{std::move(copy)});
auto [t, ok] = normalize(res, scope, *arena, *iceHandler);
if (!ok)
normalizationTooComplex = true;
return t;
}
else
return anyType;
}
TypePackId Anyification::clean(TypePackId tp)
{
LUAU_ASSERT(isDirty(tp));
return anyTypePack;
}
bool Anyification::ignoreChildren(TypeId ty)
{
if (FFlag::LuauClassTypeVarsInSubstitution && get<ClassTypeVar>(ty))
return true;
return ty->persistent;
}
bool Anyification::ignoreChildren(TypePackId ty)
{
return ty->persistent;
}
}

View file

@ -2,6 +2,8 @@
#include "Luau/ApplyTypeFunction.h"
LUAU_FASTFLAG(LuauClassTypeVarsInSubstitution)
namespace Luau
{
@ -31,6 +33,8 @@ bool ApplyTypeFunction::ignoreChildren(TypeId ty)
{
if (get<GenericTypeVar>(ty))
return true;
else if (FFlag::LuauClassTypeVarsInSubstitution && get<ClassTypeVar>(ty))
return true;
else
return false;
}

View file

@ -14,6 +14,8 @@
LUAU_FASTFLAG(LuauSelfCallAutocompleteFix3)
LUAU_FASTFLAGVARIABLE(LuauAutocompleteFixGlobalOrder, false)
static const std::unordered_set<std::string> kStatementStartingKeywords = {
"while", "if", "local", "repeat", "function", "do", "for", "return", "break", "continue", "type", "export"};
@ -135,11 +137,11 @@ static std::optional<TypeId> findExpectedTypeAt(const Module& module, AstNode* n
return *it;
}
static bool checkTypeMatch(TypeArena* typeArena, TypeId subTy, TypeId superTy)
static bool checkTypeMatch(TypeId subTy, TypeId superTy, NotNull<Scope> scope, TypeArena* typeArena)
{
InternalErrorReporter iceReporter;
UnifierSharedState unifierState(&iceReporter);
Unifier unifier(typeArena, Mode::Strict, Location(), Variance::Covariant, unifierState);
Unifier unifier(typeArena, Mode::Strict, scope, Location(), Variance::Covariant, unifierState);
return unifier.canUnify(subTy, superTy).empty();
}
@ -148,12 +150,14 @@ static TypeCorrectKind checkTypeCorrectKind(const Module& module, TypeArena* typ
{
ty = follow(ty);
auto canUnify = [&typeArena](TypeId subTy, TypeId superTy) {
NotNull<Scope> moduleScope{module.getModuleScope().get()};
auto canUnify = [&typeArena, moduleScope](TypeId subTy, TypeId superTy) {
LUAU_ASSERT(!FFlag::LuauSelfCallAutocompleteFix3);
InternalErrorReporter iceReporter;
UnifierSharedState unifierState(&iceReporter);
Unifier unifier(typeArena, Mode::Strict, Location(), Variance::Covariant, unifierState);
Unifier unifier(typeArena, Mode::Strict, moduleScope, Location(), Variance::Covariant, unifierState);
unifier.tryUnify(subTy, superTy);
bool ok = unifier.errors.empty();
@ -167,11 +171,11 @@ static TypeCorrectKind checkTypeCorrectKind(const Module& module, TypeArena* typ
TypeId expectedType = follow(*typeAtPosition);
auto checkFunctionType = [typeArena, &canUnify, &expectedType](const FunctionTypeVar* ftv) {
auto checkFunctionType = [typeArena, moduleScope, &canUnify, &expectedType](const FunctionTypeVar* ftv) {
if (FFlag::LuauSelfCallAutocompleteFix3)
{
if (std::optional<TypeId> firstRetTy = first(ftv->retTypes))
return checkTypeMatch(typeArena, *firstRetTy, expectedType);
return checkTypeMatch(*firstRetTy, expectedType, moduleScope, typeArena);
return false;
}
@ -210,7 +214,7 @@ static TypeCorrectKind checkTypeCorrectKind(const Module& module, TypeArena* typ
}
if (FFlag::LuauSelfCallAutocompleteFix3)
return checkTypeMatch(typeArena, ty, expectedType) ? TypeCorrectKind::Correct : TypeCorrectKind::None;
return checkTypeMatch(ty, expectedType, NotNull{module.getModuleScope().get()}, typeArena) ? TypeCorrectKind::Correct : TypeCorrectKind::None;
else
return canUnify(ty, expectedType) ? TypeCorrectKind::Correct : TypeCorrectKind::None;
}
@ -268,7 +272,7 @@ static void autocompleteProps(const Module& module, TypeArena* typeArena, TypeId
return colonIndex;
}
};
auto isWrongIndexer = [typeArena, rootTy, indexType](Luau::TypeId type) {
auto isWrongIndexer = [typeArena, &module, rootTy, indexType](Luau::TypeId type) {
LUAU_ASSERT(FFlag::LuauSelfCallAutocompleteFix3);
if (indexType == PropIndexType::Key)
@ -276,7 +280,7 @@ static void autocompleteProps(const Module& module, TypeArena* typeArena, TypeId
bool calledWithSelf = indexType == PropIndexType::Colon;
auto isCompatibleCall = [typeArena, rootTy, calledWithSelf](const FunctionTypeVar* ftv) {
auto isCompatibleCall = [typeArena, &module, rootTy, calledWithSelf](const FunctionTypeVar* ftv) {
// Strong match with definition is a success
if (calledWithSelf == ftv->hasSelf)
return true;
@ -289,7 +293,7 @@ static void autocompleteProps(const Module& module, TypeArena* typeArena, TypeId
// When called with '.', but declared with 'self', it is considered invalid if first argument is compatible
if (std::optional<TypeId> firstArgTy = first(ftv->argTypes))
{
if (checkTypeMatch(typeArena, rootTy, *firstArgTy))
if (checkTypeMatch(rootTy, *firstArgTy, NotNull{module.getModuleScope().get()}, typeArena))
return calledWithSelf;
}
@ -1073,10 +1077,21 @@ T* extractStat(const std::vector<AstNode*>& ancestry)
return nullptr;
}
static bool isBindingLegalAtCurrentPosition(const Binding& binding, Position pos)
static bool isBindingLegalAtCurrentPosition(const Symbol& symbol, const Binding& binding, Position pos)
{
// Default Location used for global bindings, which are always legal.
return binding.location == Location() || binding.location.end < pos;
if (FFlag::LuauAutocompleteFixGlobalOrder)
{
if (symbol.local)
return binding.location.end < pos;
// Builtin globals have an empty location; for defined globals, we want pos to be outside of the definition range to suggest it
return binding.location == Location() || !binding.location.containsClosed(pos);
}
else
{
// Default Location used for global bindings, which are always legal.
return binding.location == Location() || binding.location.end < pos;
}
}
static AutocompleteEntryMap autocompleteStatement(
@ -1097,7 +1112,7 @@ static AutocompleteEntryMap autocompleteStatement(
{
for (const auto& [name, binding] : scope->bindings)
{
if (!isBindingLegalAtCurrentPosition(binding, position))
if (!isBindingLegalAtCurrentPosition(name, binding, position))
continue;
std::string n = toString(name);
@ -1200,7 +1215,7 @@ static bool autocompleteIfElseExpression(
}
}
static void autocompleteExpression(const SourceModule& sourceModule, const Module& module, const TypeChecker& typeChecker, TypeArena* typeArena,
static AutocompleteContext autocompleteExpression(const SourceModule& sourceModule, const Module& module, const TypeChecker& typeChecker, TypeArena* typeArena,
const std::vector<AstNode*>& ancestry, Position position, AutocompleteEntryMap& result)
{
LUAU_ASSERT(!ancestry.empty());
@ -1213,9 +1228,9 @@ static void autocompleteExpression(const SourceModule& sourceModule, const Modul
autocompleteProps(module, typeArena, *it, PropIndexType::Point, ancestry, result);
}
else if (autocompleteIfElseExpression(node, ancestry, position, result))
return;
return AutocompleteContext::Keyword;
else if (node->is<AstExprFunction>())
return;
return AutocompleteContext::Unknown;
else
{
// This is inefficient. :(
@ -1225,7 +1240,7 @@ static void autocompleteExpression(const SourceModule& sourceModule, const Modul
{
for (const auto& [name, binding] : scope->bindings)
{
if (!isBindingLegalAtCurrentPosition(binding, position))
if (!isBindingLegalAtCurrentPosition(name, binding, position))
continue;
if (isBeingDefined(ancestry, name))
@ -1260,14 +1275,16 @@ static void autocompleteExpression(const SourceModule& sourceModule, const Modul
if (auto ty = findExpectedTypeAt(module, node, position))
autocompleteStringSingleton(*ty, true, result);
}
return AutocompleteContext::Expression;
}
static AutocompleteEntryMap autocompleteExpression(const SourceModule& sourceModule, const Module& module, const TypeChecker& typeChecker,
static AutocompleteResult autocompleteExpression(const SourceModule& sourceModule, const Module& module, const TypeChecker& typeChecker,
TypeArena* typeArena, const std::vector<AstNode*>& ancestry, Position position)
{
AutocompleteEntryMap result;
autocompleteExpression(sourceModule, module, typeChecker, typeArena, ancestry, position, result);
return result;
AutocompleteContext context = autocompleteExpression(sourceModule, module, typeChecker, typeArena, ancestry, position, result);
return {result, ancestry, context};
}
static std::optional<const ClassTypeVar*> getMethodContainingClass(const ModulePtr& module, AstExpr* funcExpr)
@ -1406,27 +1423,27 @@ static AutocompleteResult autocomplete(const SourceModule& sourceModule, const M
if (!FFlag::LuauSelfCallAutocompleteFix3 && isString(ty))
return {
autocompleteProps(*module, typeArena, typeChecker.globalScope->bindings[AstName{"string"}].typeId, indexType, ancestry), ancestry};
autocompleteProps(*module, typeArena, typeChecker.globalScope->bindings[AstName{"string"}].typeId, indexType, ancestry), ancestry, AutocompleteContext::Property};
else
return {autocompleteProps(*module, typeArena, ty, indexType, ancestry), ancestry};
return {autocompleteProps(*module, typeArena, ty, indexType, ancestry), ancestry, AutocompleteContext::Property};
}
else if (auto typeReference = node->as<AstTypeReference>())
{
if (typeReference->prefix)
return {autocompleteModuleTypes(*module, position, typeReference->prefix->value), ancestry};
return {autocompleteModuleTypes(*module, position, typeReference->prefix->value), ancestry, AutocompleteContext::Type};
else
return {autocompleteTypeNames(*module, position, ancestry), ancestry};
return {autocompleteTypeNames(*module, position, ancestry), ancestry, AutocompleteContext::Type};
}
else if (node->is<AstTypeError>())
{
return {autocompleteTypeNames(*module, position, ancestry), ancestry};
return {autocompleteTypeNames(*module, position, ancestry), ancestry, AutocompleteContext::Type};
}
else if (AstStatLocal* statLocal = node->as<AstStatLocal>())
{
if (statLocal->vars.size == 1 && (!statLocal->equalsSignLocation || position < statLocal->equalsSignLocation->begin))
return {{{"function", AutocompleteEntry{AutocompleteEntryKind::Keyword}}}, ancestry};
return {{{"function", AutocompleteEntry{AutocompleteEntryKind::Keyword}}}, ancestry, AutocompleteContext::Unknown};
else if (statLocal->equalsSignLocation && position >= statLocal->equalsSignLocation->end)
return {autocompleteExpression(sourceModule, *module, typeChecker, typeArena, ancestry, position), ancestry};
return autocompleteExpression(sourceModule, *module, typeChecker, typeArena, ancestry, position);
else
return {};
}
@ -1436,16 +1453,16 @@ static AutocompleteResult autocomplete(const SourceModule& sourceModule, const M
if (!statFor->hasDo || position < statFor->doLocation.begin)
{
if (!statFor->from->is<AstExprError>() && !statFor->to->is<AstExprError>() && (!statFor->step || !statFor->step->is<AstExprError>()))
return {{{"do", AutocompleteEntry{AutocompleteEntryKind::Keyword}}}, ancestry};
return {{{"do", AutocompleteEntry{AutocompleteEntryKind::Keyword}}}, ancestry, AutocompleteContext::Keyword};
if (statFor->from->location.containsClosed(position) || statFor->to->location.containsClosed(position) ||
(statFor->step && statFor->step->location.containsClosed(position)))
return {autocompleteExpression(sourceModule, *module, typeChecker, typeArena, ancestry, position), ancestry};
return autocompleteExpression(sourceModule, *module, typeChecker, typeArena, ancestry, position);
return {};
}
return {autocompleteStatement(sourceModule, *module, ancestry, position), ancestry};
return {autocompleteStatement(sourceModule, *module, ancestry, position), ancestry, AutocompleteContext::Statement};
}
else if (AstStatForIn* statForIn = parent->as<AstStatForIn>(); statForIn && (node->is<AstStatBlock>() || isIdentifier(node)))
@ -1461,7 +1478,7 @@ static AutocompleteResult autocomplete(const SourceModule& sourceModule, const M
return {};
}
return {{{"in", AutocompleteEntry{AutocompleteEntryKind::Keyword}}}, ancestry};
return {{{"in", AutocompleteEntry{AutocompleteEntryKind::Keyword}}}, ancestry, AutocompleteContext::Keyword};
}
if (!statForIn->hasDo || position <= statForIn->doLocation.begin)
@ -1470,10 +1487,10 @@ static AutocompleteResult autocomplete(const SourceModule& sourceModule, const M
AstExpr* lastExpr = statForIn->values.data[statForIn->values.size - 1];
if (lastExpr->location.containsClosed(position))
return {autocompleteExpression(sourceModule, *module, typeChecker, typeArena, ancestry, position), ancestry};
return autocompleteExpression(sourceModule, *module, typeChecker, typeArena, ancestry, position);
if (position > lastExpr->location.end)
return {{{"do", AutocompleteEntry{AutocompleteEntryKind::Keyword}}}, ancestry};
return {{{"do", AutocompleteEntry{AutocompleteEntryKind::Keyword}}}, ancestry, AutocompleteContext::Keyword};
return {}; // Not sure what this means
}
@ -1483,45 +1500,45 @@ static AutocompleteResult autocomplete(const SourceModule& sourceModule, const M
// The AST looks a bit differently if the cursor is at a position where only the "do" keyword is allowed.
// ex "for f in f do"
if (!statForIn->hasDo)
return {{{"do", AutocompleteEntry{AutocompleteEntryKind::Keyword}}}, ancestry};
return {{{"do", AutocompleteEntry{AutocompleteEntryKind::Keyword}}}, ancestry, AutocompleteContext::Keyword};
return {autocompleteStatement(sourceModule, *module, ancestry, position), ancestry};
return {autocompleteStatement(sourceModule, *module, ancestry, position), ancestry, AutocompleteContext::Statement};
}
else if (AstStatWhile* statWhile = parent->as<AstStatWhile>(); node->is<AstStatBlock>() && statWhile)
{
if (!statWhile->hasDo && !statWhile->condition->is<AstStatError>() && position > statWhile->condition->location.end)
return {{{"do", AutocompleteEntry{AutocompleteEntryKind::Keyword}}}, ancestry};
return {{{"do", AutocompleteEntry{AutocompleteEntryKind::Keyword}}}, ancestry, AutocompleteContext::Keyword};
if (!statWhile->hasDo || position < statWhile->doLocation.begin)
return {autocompleteExpression(sourceModule, *module, typeChecker, typeArena, ancestry, position), ancestry};
return autocompleteExpression(sourceModule, *module, typeChecker, typeArena, ancestry, position);
if (statWhile->hasDo && position > statWhile->doLocation.end)
return {autocompleteStatement(sourceModule, *module, ancestry, position), ancestry};
return {autocompleteStatement(sourceModule, *module, ancestry, position), ancestry, AutocompleteContext::Statement};
}
else if (AstStatWhile* statWhile = extractStat<AstStatWhile>(ancestry); statWhile && !statWhile->hasDo)
return {{{"do", AutocompleteEntry{AutocompleteEntryKind::Keyword}}}, ancestry};
return {{{"do", AutocompleteEntry{AutocompleteEntryKind::Keyword}}}, ancestry, AutocompleteContext::Keyword};
else if (AstStatIf* statIf = node->as<AstStatIf>(); statIf && !statIf->elseLocation.has_value())
{
return {
{{"else", AutocompleteEntry{AutocompleteEntryKind::Keyword}}, {"elseif", AutocompleteEntry{AutocompleteEntryKind::Keyword}}}, ancestry};
{{"else", AutocompleteEntry{AutocompleteEntryKind::Keyword}}, {"elseif", AutocompleteEntry{AutocompleteEntryKind::Keyword}}}, ancestry, AutocompleteContext::Keyword};
}
else if (AstStatIf* statIf = parent->as<AstStatIf>(); statIf && node->is<AstStatBlock>())
{
if (statIf->condition->is<AstExprError>())
return {autocompleteExpression(sourceModule, *module, typeChecker, typeArena, ancestry, position), ancestry};
return autocompleteExpression(sourceModule, *module, typeChecker, typeArena, ancestry, position);
else if (!statIf->thenLocation || statIf->thenLocation->containsClosed(position))
return {{{"then", AutocompleteEntry{AutocompleteEntryKind::Keyword}}}, ancestry};
return {{{"then", AutocompleteEntry{AutocompleteEntryKind::Keyword}}}, ancestry, AutocompleteContext::Keyword};
}
else if (AstStatIf* statIf = extractStat<AstStatIf>(ancestry);
statIf && (!statIf->thenLocation || statIf->thenLocation->containsClosed(position)))
return {{{"then", AutocompleteEntry{AutocompleteEntryKind::Keyword}}}, ancestry};
return {{{"then", AutocompleteEntry{AutocompleteEntryKind::Keyword}}}, ancestry, AutocompleteContext::Keyword};
else if (AstStatRepeat* statRepeat = node->as<AstStatRepeat>(); statRepeat && statRepeat->condition->is<AstExprError>())
return {autocompleteExpression(sourceModule, *module, typeChecker, typeArena, ancestry, position), ancestry};
return autocompleteExpression(sourceModule, *module, typeChecker, typeArena, ancestry, position);
else if (AstStatRepeat* statRepeat = extractStat<AstStatRepeat>(ancestry); statRepeat)
return {autocompleteStatement(sourceModule, *module, ancestry, position), ancestry};
return {autocompleteStatement(sourceModule, *module, ancestry, position), ancestry, AutocompleteContext::Statement};
else if (AstExprTable* exprTable = parent->as<AstExprTable>(); exprTable && (node->is<AstExprGlobal>() || node->is<AstExprConstantString>()))
{
for (const auto& [kind, key, value] : exprTable->items)
@ -1547,7 +1564,7 @@ static AutocompleteResult autocomplete(const SourceModule& sourceModule, const M
if (!key)
autocompleteExpression(sourceModule, *module, typeChecker, typeArena, ancestry, position, result);
return {result, ancestry};
return {result, ancestry, AutocompleteContext::Property};
}
break;
@ -1555,11 +1572,11 @@ static AutocompleteResult autocomplete(const SourceModule& sourceModule, const M
}
}
else if (isIdentifier(node) && (parent->is<AstStatExpr>() || parent->is<AstStatError>()))
return {autocompleteStatement(sourceModule, *module, ancestry, position), ancestry};
return {autocompleteStatement(sourceModule, *module, ancestry, position), ancestry, AutocompleteContext::Statement};
if (std::optional<AutocompleteEntryMap> ret = autocompleteStringParams(sourceModule, module, ancestry, position, callback))
{
return {*ret, ancestry};
return {*ret, ancestry, AutocompleteContext::String};
}
else if (node->is<AstExprConstantString>())
{
@ -1585,7 +1602,7 @@ static AutocompleteResult autocomplete(const SourceModule& sourceModule, const M
}
}
return {result, ancestry};
return {result, ancestry, AutocompleteContext::String};
}
if (node->is<AstExprConstantNumber>())
@ -1594,9 +1611,9 @@ static AutocompleteResult autocomplete(const SourceModule& sourceModule, const M
}
if (node->asExpr())
return {autocompleteExpression(sourceModule, *module, typeChecker, typeArena, ancestry, position), ancestry};
return autocompleteExpression(sourceModule, *module, typeChecker, typeArena, ancestry, position);
else if (node->asStat())
return {autocompleteStatement(sourceModule, *module, ancestry, position), ancestry};
return {autocompleteStatement(sourceModule, *module, ancestry, position), ancestry, AutocompleteContext::Statement};
return {};
}

View file

@ -7,6 +7,7 @@
#include "Luau/Unifiable.h"
LUAU_FASTFLAG(DebugLuauCopyBeforeNormalizing)
LUAU_FASTFLAG(LuauClonePublicInterfaceLess)
LUAU_FASTINTVARIABLE(LuauTypeCloneRecursionLimit, 300)
@ -445,7 +446,7 @@ TypeFun clone(const TypeFun& typeFun, TypeArena& dest, CloneState& cloneState)
return result;
}
TypeId shallowClone(TypeId ty, TypeArena& dest, const TxnLog* log)
TypeId shallowClone(TypeId ty, TypeArena& dest, const TxnLog* log, bool alwaysClone)
{
ty = log->follow(ty);
@ -504,6 +505,15 @@ TypeId shallowClone(TypeId ty, TypeArena& dest, const TxnLog* log)
PendingExpansionTypeVar clone{petv->fn, petv->typeArguments, petv->packArguments};
result = dest.addType(std::move(clone));
}
else if (const ClassTypeVar* ctv = get<ClassTypeVar>(ty); FFlag::LuauClonePublicInterfaceLess && ctv && alwaysClone)
{
ClassTypeVar clone{ctv->name, ctv->props, ctv->parent, ctv->metatable, ctv->tags, ctv->userData, ctv->definitionModuleName};
result = dest.addType(std::move(clone));
}
else if (FFlag::LuauClonePublicInterfaceLess && alwaysClone)
{
result = dest.addType(*ty);
}
else
return result;

View file

@ -5,8 +5,9 @@
namespace Luau
{
Constraint::Constraint(ConstraintV&& c)
Constraint::Constraint(ConstraintV&& c, NotNull<Scope> scope)
: c(std::move(c))
, scope(scope)
{
}

View file

@ -1,6 +1,9 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#include "Luau/ConstraintGraphBuilder.h"
#include "Luau/Ast.h"
#include "Luau/Common.h"
#include "Luau/Constraint.h"
#include "Luau/RecursionCounter.h"
#include "Luau/ToString.h"
@ -14,8 +17,9 @@ namespace Luau
const AstStat* getFallthrough(const AstStat* node); // TypeInfer.cpp
ConstraintGraphBuilder::ConstraintGraphBuilder(
const ModuleName& moduleName, TypeArena* arena, NotNull<InternalErrorReporter> ice, NotNull<Scope> globalScope)
const ModuleName& moduleName, ModulePtr module, TypeArena* arena, NotNull<InternalErrorReporter> ice, const ScopePtr& globalScope)
: moduleName(moduleName)
, module(module)
, singletonTypes(getSingletonTypes())
, arena(arena)
, rootScope(nullptr)
@ -23,6 +27,7 @@ ConstraintGraphBuilder::ConstraintGraphBuilder(
, globalScope(globalScope)
{
LUAU_ASSERT(arena);
LUAU_ASSERT(module);
}
TypeId ConstraintGraphBuilder::freshType(const ScopePtr& scope)
@ -36,20 +41,22 @@ TypePackId ConstraintGraphBuilder::freshTypePack(const ScopePtr& scope)
return arena->addTypePack(TypePackVar{std::move(f)});
}
ScopePtr ConstraintGraphBuilder::childScope(Location location, const ScopePtr& parent)
ScopePtr ConstraintGraphBuilder::childScope(AstNode* node, const ScopePtr& parent)
{
auto scope = std::make_shared<Scope>(parent);
scopes.emplace_back(location, scope);
scopes.emplace_back(node->location, scope);
scope->returnType = parent->returnType;
parent->children.push_back(NotNull(scope.get()));
parent->children.push_back(NotNull{scope.get()});
module->astScopes[node] = scope.get();
return scope;
}
void ConstraintGraphBuilder::addConstraint(const ScopePtr& scope, ConstraintV cv)
{
scope->constraints.emplace_back(new Constraint{std::move(cv)});
scope->constraints.emplace_back(new Constraint{std::move(cv), NotNull{scope.get()}});
}
void ConstraintGraphBuilder::addConstraint(const ScopePtr& scope, std::unique_ptr<Constraint> c)
@ -61,20 +68,21 @@ void ConstraintGraphBuilder::visit(AstStatBlock* block)
{
LUAU_ASSERT(scopes.empty());
LUAU_ASSERT(rootScope == nullptr);
ScopePtr scope = std::make_shared<Scope>(singletonTypes.anyTypePack);
ScopePtr scope = std::make_shared<Scope>(globalScope);
rootScope = scope.get();
scopes.emplace_back(block->location, scope);
module->astScopes[block] = NotNull{scope.get()};
rootScope->returnType = freshTypePack(scope);
prepopulateGlobalScope(scope, block);
// TODO: We should share the global scope.
rootScope->typeBindings["nil"] = TypeFun{singletonTypes.nilType};
rootScope->typeBindings["number"] = TypeFun{singletonTypes.numberType};
rootScope->typeBindings["string"] = TypeFun{singletonTypes.stringType};
rootScope->typeBindings["boolean"] = TypeFun{singletonTypes.booleanType};
rootScope->typeBindings["thread"] = TypeFun{singletonTypes.threadType};
rootScope->privateTypeBindings["nil"] = TypeFun{singletonTypes.nilType};
rootScope->privateTypeBindings["number"] = TypeFun{singletonTypes.numberType};
rootScope->privateTypeBindings["string"] = TypeFun{singletonTypes.stringType};
rootScope->privateTypeBindings["boolean"] = TypeFun{singletonTypes.booleanType};
rootScope->privateTypeBindings["thread"] = TypeFun{singletonTypes.threadType};
visitBlockWithoutChildScope(scope, block);
}
@ -99,7 +107,7 @@ void ConstraintGraphBuilder::visitBlockWithoutChildScope(const ScopePtr& scope,
{
if (auto alias = stat->as<AstStatTypeAlias>())
{
if (scope->typeBindings.count(alias->name.value) != 0)
if (scope->privateTypeBindings.count(alias->name.value) != 0)
{
auto it = aliasDefinitionLocations.find(alias->name.value);
LUAU_ASSERT(it != aliasDefinitionLocations.end());
@ -112,7 +120,7 @@ void ConstraintGraphBuilder::visitBlockWithoutChildScope(const ScopePtr& scope,
ScopePtr defnScope = scope;
if (hasGenerics)
{
defnScope = childScope(alias->location, scope);
defnScope = childScope(alias, scope);
}
TypeId initialType = freshType(scope);
@ -121,16 +129,16 @@ void ConstraintGraphBuilder::visitBlockWithoutChildScope(const ScopePtr& scope,
for (const auto& [name, gen] : createGenerics(defnScope, alias->generics))
{
initialFun.typeParams.push_back(gen);
defnScope->typeBindings[name] = TypeFun{gen.ty};
defnScope->privateTypeBindings[name] = TypeFun{gen.ty};
}
for (const auto& [name, genPack] : createGenericPacks(defnScope, alias->genericPacks))
{
initialFun.typePackParams.push_back(genPack);
defnScope->typePackBindings[name] = genPack.tp;
defnScope->privateTypePackBindings[name] = genPack.tp;
}
scope->typeBindings[alias->name.value] = std::move(initialFun);
scope->privateTypeBindings[alias->name.value] = std::move(initialFun);
astTypeAliasDefiningScopes[alias] = defnScope;
aliasDefinitionLocations[alias->name.value] = alias->location;
}
@ -150,6 +158,10 @@ void ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStat* stat)
visit(scope, s);
else if (auto s = stat->as<AstStatFor>())
visit(scope, s);
else if (auto s = stat->as<AstStatWhile>())
visit(scope, s);
else if (auto s = stat->as<AstStatRepeat>())
visit(scope, s);
else if (auto f = stat->as<AstStatFunction>())
visit(scope, f);
else if (auto f = stat->as<AstStatLocalFunction>())
@ -158,6 +170,8 @@ void ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStat* stat)
visit(scope, r);
else if (auto a = stat->as<AstStatAssign>())
visit(scope, a);
else if (auto a = stat->as<AstStatCompoundAssign>())
visit(scope, a);
else if (auto e = stat->as<AstStatExpr>())
checkPack(scope, e->expr);
else if (auto i = stat->as<AstStatIf>())
@ -236,12 +250,32 @@ void ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatFor* for_)
checkNumber(for_->to);
checkNumber(for_->step);
ScopePtr forScope = childScope(for_->location, scope);
ScopePtr forScope = childScope(for_, scope);
forScope->bindings[for_->var] = Binding{singletonTypes.numberType, for_->var->location};
visit(forScope, for_->body);
}
void ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatWhile* while_)
{
check(scope, while_->condition);
ScopePtr whileScope = childScope(while_, scope);
visit(whileScope, while_->body);
}
void ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatRepeat* repeat)
{
ScopePtr repeatScope = childScope(repeat, scope);
visit(repeatScope, repeat->body);
// The condition does indeed have access to bindings from within the body of
// the loop.
check(repeatScope, repeat->condition);
}
void addConstraints(Constraint* constraint, NotNull<Scope> scope)
{
scope->constraints.reserve(scope->constraints.size() + scope->constraints.size());
@ -272,8 +306,8 @@ void ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatLocalFunction*
checkFunctionBody(sig.bodyScope, function->func);
std::unique_ptr<Constraint> c{
new Constraint{GeneralizationConstraint{functionType, sig.signature, sig.signatureScope ? sig.signatureScope.get() : sig.bodyScope.get()}}};
NotNull<Scope> constraintScope{sig.signatureScope ? sig.signatureScope.get() : sig.bodyScope.get()};
std::unique_ptr<Constraint> c = std::make_unique<Constraint>(GeneralizationConstraint{functionType, sig.signature}, constraintScope);
addConstraints(c.get(), NotNull(sig.bodyScope.get()));
addConstraint(scope, std::move(c));
@ -342,8 +376,8 @@ void ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatFunction* funct
checkFunctionBody(sig.bodyScope, function->func);
std::unique_ptr<Constraint> c{
new Constraint{GeneralizationConstraint{functionType, sig.signature, sig.signatureScope ? sig.signatureScope.get() : sig.bodyScope.get()}}};
NotNull<Scope> constraintScope{sig.signatureScope ? sig.signatureScope.get() : sig.bodyScope.get()};
std::unique_ptr<Constraint> c = std::make_unique<Constraint>(GeneralizationConstraint{functionType, sig.signature}, constraintScope);
addConstraints(c.get(), NotNull(sig.bodyScope.get()));
addConstraint(scope, std::move(c));
@ -357,7 +391,7 @@ void ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatReturn* ret)
void ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatBlock* block)
{
ScopePtr innerScope = childScope(block->location, scope);
ScopePtr innerScope = childScope(block, scope);
visitBlockWithoutChildScope(innerScope, block);
}
@ -370,16 +404,30 @@ void ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatAssign* assign)
addConstraint(scope, PackSubtypeConstraint{valuePack, varPackId});
}
void ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatCompoundAssign* assign)
{
// Synthesize A = A op B from A op= B and then build constraints for that instead.
AstExprBinary exprBinary{assign->location, assign->op, assign->var, assign->value};
AstExpr* exprBinaryPtr = &exprBinary;
AstArray<AstExpr*> vars{&assign->var, 1};
AstArray<AstExpr*> values{&exprBinaryPtr, 1};
AstStatAssign syntheticAssign{assign->location, vars, values};
visit(scope, &syntheticAssign);
}
void ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatIf* ifStatement)
{
check(scope, ifStatement->condition);
ScopePtr thenScope = childScope(ifStatement->thenbody->location, scope);
ScopePtr thenScope = childScope(ifStatement->thenbody, scope);
visit(thenScope, ifStatement->thenbody);
if (ifStatement->elsebody)
{
ScopePtr elseScope = childScope(ifStatement->elsebody->location, scope);
ScopePtr elseScope = childScope(ifStatement->elsebody, scope);
visit(elseScope, ifStatement->elsebody);
}
}
@ -388,11 +436,11 @@ void ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatTypeAlias* alia
{
// TODO: Exported type aliases
auto bindingIt = scope->typeBindings.find(alias->name.value);
auto bindingIt = scope->privateTypeBindings.find(alias->name.value);
ScopePtr* defnIt = astTypeAliasDefiningScopes.find(alias);
// These will be undefined if the alias was a duplicate definition, in which
// case we just skip over it.
if (bindingIt == scope->typeBindings.end() || defnIt == nullptr)
if (bindingIt == scope->privateTypeBindings.end() || defnIt == nullptr)
{
return;
}
@ -416,17 +464,152 @@ void ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatDeclareGlobal*
LUAU_ASSERT(global->type);
TypeId globalTy = resolveType(scope, global->type);
Name globalName(global->name.value);
module->declaredGlobals[globalName] = globalTy;
scope->bindings[global->name] = Binding{globalTy, global->location};
}
void ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatDeclareClass* global)
static bool isMetamethod(const Name& name)
{
LUAU_ASSERT(false); // TODO: implement
return name == "__index" || name == "__newindex" || name == "__call" || name == "__concat" || name == "__unm" || name == "__add" ||
name == "__sub" || name == "__mul" || name == "__div" || name == "__mod" || name == "__pow" || name == "__tostring" ||
name == "__metatable" || name == "__eq" || name == "__lt" || name == "__le" || name == "__mode" || name == "__iter" || name == "__len";
}
void ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatDeclareClass* declaredClass)
{
std::optional<TypeId> superTy = std::nullopt;
if (declaredClass->superName)
{
Name superName = Name(declaredClass->superName->value);
std::optional<TypeFun> lookupType = scope->lookupType(superName);
if (!lookupType)
{
reportError(declaredClass->location, UnknownSymbol{superName, UnknownSymbol::Type});
return;
}
// We don't have generic classes, so this assertion _should_ never be hit.
LUAU_ASSERT(lookupType->typeParams.size() == 0 && lookupType->typePackParams.size() == 0);
superTy = lookupType->type;
if (!get<ClassTypeVar>(follow(*superTy)))
{
reportError(declaredClass->location,
GenericError{format("Cannot use non-class type '%s' as a superclass of class '%s'", superName.c_str(), declaredClass->name.value)});
return;
}
}
Name className(declaredClass->name.value);
TypeId classTy = arena->addType(ClassTypeVar(className, {}, superTy, std::nullopt, {}, {}, moduleName));
ClassTypeVar* ctv = getMutable<ClassTypeVar>(classTy);
TypeId metaTy = arena->addType(TableTypeVar{TableState::Sealed, scope->level});
TableTypeVar* metatable = getMutable<TableTypeVar>(metaTy);
ctv->metatable = metaTy;
scope->exportedTypeBindings[className] = TypeFun{{}, classTy};
for (const AstDeclaredClassProp& prop : declaredClass->props)
{
Name propName(prop.name.value);
TypeId propTy = resolveType(scope, prop.ty);
bool assignToMetatable = isMetamethod(propName);
// Function types always take 'self', but this isn't reflected in the
// parsed annotation. Add it here.
if (prop.isMethod)
{
if (FunctionTypeVar* ftv = getMutable<FunctionTypeVar>(propTy))
{
ftv->argNames.insert(ftv->argNames.begin(), FunctionArgument{"self", {}});
ftv->argTypes = arena->addTypePack(TypePack{{classTy}, ftv->argTypes});
ftv->hasSelf = true;
}
}
if (ctv->props.count(propName) == 0)
{
if (assignToMetatable)
metatable->props[propName] = {propTy};
else
ctv->props[propName] = {propTy};
}
else
{
TypeId currentTy = assignToMetatable ? metatable->props[propName].type : ctv->props[propName].type;
// We special-case this logic to keep the intersection flat; otherwise we
// would create a ton of nested intersection types.
if (const IntersectionTypeVar* itv = get<IntersectionTypeVar>(currentTy))
{
std::vector<TypeId> options = itv->parts;
options.push_back(propTy);
TypeId newItv = arena->addType(IntersectionTypeVar{std::move(options)});
if (assignToMetatable)
metatable->props[propName] = {newItv};
else
ctv->props[propName] = {newItv};
}
else if (get<FunctionTypeVar>(currentTy))
{
TypeId intersection = arena->addType(IntersectionTypeVar{{currentTy, propTy}});
if (assignToMetatable)
metatable->props[propName] = {intersection};
else
ctv->props[propName] = {intersection};
}
else
{
reportError(declaredClass->location, GenericError{format("Cannot overload non-function class member '%s'", propName.c_str())});
}
}
}
}
void ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatDeclareFunction* global)
{
LUAU_ASSERT(false); // TODO: implement
std::vector<std::pair<Name, GenericTypeDefinition>> generics = createGenerics(scope, global->generics);
std::vector<std::pair<Name, GenericTypePackDefinition>> genericPacks = createGenericPacks(scope, global->genericPacks);
std::vector<TypeId> genericTys;
genericTys.reserve(generics.size());
for (auto& [name, generic] : generics)
genericTys.push_back(generic.ty);
std::vector<TypePackId> genericTps;
genericTps.reserve(genericPacks.size());
for (auto& [name, generic] : genericPacks)
genericTps.push_back(generic.tp);
ScopePtr funScope = scope;
if (!generics.empty() || !genericPacks.empty())
funScope = childScope(global, scope);
TypePackId paramPack = resolveTypePack(funScope, global->params);
TypePackId retPack = resolveTypePack(funScope, global->retTypes);
TypeId fnType = arena->addType(FunctionTypeVar{funScope->level, std::move(genericTys), std::move(genericTps), paramPack, retPack});
FunctionTypeVar* ftv = getMutable<FunctionTypeVar>(fnType);
ftv->argNames.reserve(global->paramNames.size);
for (const auto& el : global->paramNames)
ftv->argNames.push_back(FunctionArgument{el.first.value, el.second});
Name fnName(global->name.value);
module->declaredGlobals[fnName] = fnType;
scope->bindings[global->name] = Binding{fnType, global->location};
}
TypePackId ConstraintGraphBuilder::checkPack(const ScopePtr& scope, AstArray<AstExpr*> exprs)
@ -590,6 +773,10 @@ TypeId ConstraintGraphBuilder::check(const ScopePtr& scope, AstExpr* expr)
result = check(scope, unary);
else if (auto binary = expr->as<AstExprBinary>())
result = check(scope, binary);
else if (auto ifElse = expr->as<AstExprIfElse>())
result = check(scope, ifElse);
else if (auto typeAssert = expr->as<AstExprTypeAssertion>())
result = check(scope, typeAssert);
else if (auto err = expr->as<AstExprError>())
{
// Open question: Should we traverse into this?
@ -668,6 +855,12 @@ TypeId ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprBinary* binar
addConstraint(scope, SubtypeConstraint{leftType, rightType});
return leftType;
}
case AstExprBinary::Add:
{
TypeId resultType = arena->addType(BlockedTypeVar{});
addConstraint(scope, BinaryConstraint{AstExprBinary::Add, leftType, rightType, resultType});
return resultType;
}
case AstExprBinary::Sub:
{
TypeId resultType = arena->addType(BlockedTypeVar{});
@ -682,6 +875,30 @@ TypeId ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprBinary* binar
return nullptr;
}
TypeId ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprIfElse* ifElse)
{
check(scope, ifElse->condition);
TypeId thenType = check(scope, ifElse->trueExpr);
TypeId elseType = check(scope, ifElse->falseExpr);
if (ifElse->hasElse)
{
TypeId resultType = arena->addType(BlockedTypeVar{});
addConstraint(scope, SubtypeConstraint{thenType, resultType});
addConstraint(scope, SubtypeConstraint{elseType, resultType});
return resultType;
}
return thenType;
}
TypeId ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprTypeAssertion* typeAssert)
{
check(scope, typeAssert->expr);
return resolveType(scope, typeAssert->annotation);
}
TypeId ConstraintGraphBuilder::checkExprTable(const ScopePtr& scope, AstExprTable* expr)
{
TypeId ty = arena->addType(TableTypeVar{});
@ -748,14 +965,14 @@ ConstraintGraphBuilder::FunctionSignature ConstraintGraphBuilder::checkFunctionS
// generics properly.
if (hasGenerics)
{
signatureScope = childScope(fn->location, parent);
signatureScope = childScope(fn, parent);
// We need to assign returnType before creating bodyScope so that the
// return type gets propogated to bodyScope.
returnType = freshTypePack(signatureScope);
signatureScope->returnType = returnType;
bodyScope = childScope(fn->body->location, signatureScope);
bodyScope = childScope(fn->body, signatureScope);
std::vector<std::pair<Name, GenericTypeDefinition>> genericDefinitions = createGenerics(signatureScope, fn->generics);
std::vector<std::pair<Name, GenericTypePackDefinition>> genericPackDefinitions = createGenericPacks(signatureScope, fn->genericPacks);
@ -765,18 +982,18 @@ ConstraintGraphBuilder::FunctionSignature ConstraintGraphBuilder::checkFunctionS
for (const auto& [name, g] : genericDefinitions)
{
genericTypes.push_back(g.ty);
signatureScope->typeBindings[name] = TypeFun{g.ty};
signatureScope->privateTypeBindings[name] = TypeFun{g.ty};
}
for (const auto& [name, g] : genericPackDefinitions)
{
genericTypePacks.push_back(g.tp);
signatureScope->typePackBindings[name] = g.tp;
signatureScope->privateTypePackBindings[name] = g.tp;
}
}
else
{
bodyScope = childScope(fn->body->location, parent);
bodyScope = childScope(fn->body, parent);
returnType = freshTypePack(bodyScope);
bodyScope->returnType = returnType;
@ -851,7 +1068,7 @@ TypeId ConstraintGraphBuilder::resolveType(const ScopePtr& scope, AstType* ty, b
// TODO: Support imported types w/ require tracing.
LUAU_ASSERT(!ref->prefix);
std::optional<TypeFun> alias = scope->lookupTypeBinding(ref->name.value);
std::optional<TypeFun> alias = scope->lookupType(ref->name.value);
if (alias.has_value())
{
@ -941,7 +1158,7 @@ TypeId ConstraintGraphBuilder::resolveType(const ScopePtr& scope, AstType* ty, b
// for the generic bindings to live on.
if (hasGenerics)
{
signatureScope = childScope(fn->location, scope);
signatureScope = childScope(fn, scope);
std::vector<std::pair<Name, GenericTypeDefinition>> genericDefinitions = createGenerics(signatureScope, fn->generics);
std::vector<std::pair<Name, GenericTypePackDefinition>> genericPackDefinitions = createGenericPacks(signatureScope, fn->genericPacks);
@ -949,13 +1166,13 @@ TypeId ConstraintGraphBuilder::resolveType(const ScopePtr& scope, AstType* ty, b
for (const auto& [name, g] : genericDefinitions)
{
genericTypes.push_back(g.ty);
signatureScope->typeBindings[name] = TypeFun{g.ty};
signatureScope->privateTypeBindings[name] = TypeFun{g.ty};
}
for (const auto& [name, g] : genericPackDefinitions)
{
genericTypePacks.push_back(g.tp);
signatureScope->typePackBindings[name] = g.tp;
signatureScope->privateTypePackBindings[name] = g.tp;
}
}
else
@ -1059,7 +1276,7 @@ TypePackId ConstraintGraphBuilder::resolveTypePack(const ScopePtr& scope, AstTyp
}
else if (auto gen = tp->as<AstTypePackGeneric>())
{
if (std::optional<TypePackId> lookup = scope->lookupTypePackBinding(gen->genericName.value))
if (std::optional<TypePackId> lookup = scope->lookupPack(gen->genericName.value))
{
result = *lookup;
}

View file

@ -373,7 +373,7 @@ bool ConstraintSolver::tryDispatch(const SubtypeConstraint& c, NotNull<const Con
else if (isBlocked(c.superType))
return block(c.superType, constraint);
unify(c.subType, c.superType);
unify(c.subType, c.superType, constraint->scope);
unblock(c.subType);
unblock(c.superType);
@ -383,7 +383,7 @@ bool ConstraintSolver::tryDispatch(const SubtypeConstraint& c, NotNull<const Con
bool ConstraintSolver::tryDispatch(const PackSubtypeConstraint& c, NotNull<const Constraint> constraint, bool force)
{
unify(c.subPack, c.superPack);
unify(c.subPack, c.superPack, constraint->scope);
unblock(c.subPack);
unblock(c.superPack);
@ -398,9 +398,9 @@ bool ConstraintSolver::tryDispatch(const GeneralizationConstraint& c, NotNull<co
if (isBlocked(c.generalizedType))
asMutable(c.generalizedType)->ty.emplace<BoundTypeVar>(c.sourceType);
else
unify(c.generalizedType, c.sourceType);
unify(c.generalizedType, c.sourceType, constraint->scope);
TypeId generalized = quantify(arena, c.sourceType, c.scope);
TypeId generalized = quantify(arena, c.sourceType, constraint->scope);
*asMutable(c.sourceType) = *generalized;
unblock(c.generalizedType);
@ -422,7 +422,7 @@ bool ConstraintSolver::tryDispatch(const InstantiationConstraint& c, NotNull<con
if (isBlocked(c.subType))
asMutable(c.subType)->ty.emplace<BoundTypeVar>(*instantiated);
else
unify(c.subType, *instantiated);
unify(c.subType, *instantiated, constraint->scope);
unblock(c.subType);
@ -465,7 +465,7 @@ bool ConstraintSolver::tryDispatch(const BinaryConstraint& c, NotNull<const Cons
if (isNumber(leftType))
{
unify(leftType, rightType);
unify(leftType, rightType, constraint->scope);
asMutable(c.resultType)->ty.emplace<BoundTypeVar>(leftType);
return true;
}
@ -484,6 +484,10 @@ bool ConstraintSolver::tryDispatch(const NameConstraint& c, NotNull<const Constr
return block(c.namedType, constraint);
TypeId target = follow(c.namedType);
if (target->persistent)
return true;
if (TableTypeVar* ttv = getMutable<TableTypeVar>(target))
ttv->name = c.name;
else if (MetatableTypeVar* mtv = getMutable<MetatableTypeVar>(target))
@ -524,16 +528,18 @@ struct InstantiationQueuer : TypeVarOnceVisitor
{
ConstraintSolver* solver;
const InstantiationSignature& signature;
NotNull<Scope> scope;
explicit InstantiationQueuer(ConstraintSolver* solver, const InstantiationSignature& signature)
explicit InstantiationQueuer(ConstraintSolver* solver, const InstantiationSignature& signature, NotNull<Scope> scope)
: solver(solver)
, signature(signature)
, scope(scope)
{
}
bool visit(TypeId ty, const PendingExpansionTypeVar& petv) override
{
solver->pushConstraint(TypeAliasExpansionConstraint{ty});
solver->pushConstraint(TypeAliasExpansionConstraint{ty}, scope);
return false;
}
};
@ -637,6 +643,10 @@ bool ConstraintSolver::tryDispatch(const TypeAliasExpansionConstraint& c, NotNul
TypeId instantiated = *maybeInstantiated;
TypeId target = follow(instantiated);
if (target->persistent)
return true;
// Type function application will happily give us the exact same type if
// there are e.g. generic saturatedTypeArguments that go unused.
bool needsClone = follow(petv->fn.type) == target;
@ -678,7 +688,7 @@ bool ConstraintSolver::tryDispatch(const TypeAliasExpansionConstraint& c, NotNul
// The application is not recursive, so we need to queue up application of
// any child type function instantiations within the result in order for it
// to be complete.
InstantiationQueuer queuer{this, signature};
InstantiationQueuer queuer{this, signature, constraint->scope};
queuer.traverse(target);
instantiatedAliases[signature] = target;
@ -758,30 +768,40 @@ bool ConstraintSolver::isBlocked(NotNull<const Constraint> constraint)
return blockedIt != blockedConstraints.end() && blockedIt->second > 0;
}
void ConstraintSolver::unify(TypeId subType, TypeId superType)
void ConstraintSolver::unify(TypeId subType, TypeId superType, NotNull<Scope> scope)
{
UnifierSharedState sharedState{&iceReporter};
Unifier u{arena, Mode::Strict, Location{}, Covariant, sharedState};
Unifier u{arena, Mode::Strict, scope, Location{}, Covariant, sharedState};
u.tryUnify(subType, superType);
u.log.commit();
}
void ConstraintSolver::unify(TypePackId subPack, TypePackId superPack)
void ConstraintSolver::unify(TypePackId subPack, TypePackId superPack, NotNull<Scope> scope)
{
UnifierSharedState sharedState{&iceReporter};
Unifier u{arena, Mode::Strict, Location{}, Covariant, sharedState};
Unifier u{arena, Mode::Strict, scope, Location{}, Covariant, sharedState};
u.tryUnify(subPack, superPack);
u.log.commit();
}
void ConstraintSolver::pushConstraint(ConstraintV cv)
void ConstraintSolver::pushConstraint(ConstraintV cv, NotNull<Scope> scope)
{
std::unique_ptr<Constraint> c = std::make_unique<Constraint>(std::move(cv));
std::unique_ptr<Constraint> c = std::make_unique<Constraint>(std::move(cv), scope);
NotNull<Constraint> borrow = NotNull(c.get());
solverConstraints.push_back(std::move(c));
unsolvedConstraints.push_back(borrow);
}
void ConstraintSolver::reportError(TypeErrorData&& data, const Location& location)
{
errors.emplace_back(location, std::move(data));
}
void ConstraintSolver::reportError(TypeError e)
{
errors.emplace_back(std::move(e));
}
} // namespace Luau

View file

@ -818,27 +818,30 @@ const SourceModule* Frontend::getSourceModule(const ModuleName& moduleName) cons
return const_cast<Frontend*>(this)->getSourceModule(moduleName);
}
NotNull<Scope> Frontend::getGlobalScope()
ScopePtr Frontend::getGlobalScope()
{
if (!globalScope)
{
globalScope = typeChecker.globalScope;
}
return NotNull(globalScope.get());
return globalScope;
}
ModulePtr Frontend::check(const SourceModule& sourceModule, Mode mode, const ScopePtr& environmentScope)
{
ModulePtr result = std::make_shared<Module>();
ConstraintGraphBuilder cgb{sourceModule.name, &result->internalTypes, NotNull(&iceHandler), getGlobalScope()};
ConstraintGraphBuilder cgb{sourceModule.name, result, &result->internalTypes, NotNull(&iceHandler), getGlobalScope()};
cgb.visit(sourceModule.root);
result->errors = std::move(cgb.errors);
ConstraintSolver cs{&result->internalTypes, NotNull(cgb.rootScope)};
cs.run();
for (TypeError& e : cs.errors)
result->errors.emplace_back(std::move(e));
result->scopes = std::move(cgb.scopes);
result->astTypes = std::move(cgb.astTypes);
result->astTypePacks = std::move(cgb.astTypePacks);

View file

@ -82,6 +82,8 @@ bool ReplaceGenerics::ignoreChildren(TypeId ty)
// whenever we quantify, so the vectors overlap if and only if they are equal.
return (!generics.empty() || !genericPacks.empty()) && (ftv->generics == generics) && (ftv->genericPacks == genericPacks);
}
else if (FFlag::LuauClassTypeVarsInSubstitution && get<ClassTypeVar>(ty))
return true;
else
{
return false;

View file

@ -14,6 +14,7 @@
LUAU_FASTINTVARIABLE(LuauSuggestionDistance, 4)
LUAU_FASTFLAGVARIABLE(LuauLintGlobalNeverReadBeforeWritten, false)
LUAU_FASTFLAGVARIABLE(LuauLintComparisonPrecedence, false)
namespace Luau
{
@ -49,6 +50,7 @@ static const char* kWarningNames[] = {
"MisleadingAndOr",
"CommentDirective",
"IntegerParsing",
"ComparisonPrecedence",
};
// clang-format on
@ -2647,6 +2649,65 @@ private:
}
};
class LintComparisonPrecedence : AstVisitor
{
public:
LUAU_NOINLINE static void process(LintContext& context)
{
LintComparisonPrecedence pass;
pass.context = &context;
context.root->visit(&pass);
}
private:
LintContext* context;
bool isComparison(AstExprBinary::Op op)
{
return op == AstExprBinary::CompareNe || op == AstExprBinary::CompareEq || op == AstExprBinary::CompareLt || op == AstExprBinary::CompareLe ||
op == AstExprBinary::CompareGt || op == AstExprBinary::CompareGe;
}
bool isNot(AstExpr* node)
{
AstExprUnary* expr = node->as<AstExprUnary>();
return expr && expr->op == AstExprUnary::Not;
}
bool visit(AstExprBinary* node) override
{
if (!isComparison(node->op))
return true;
// not X == Y; we silence this for not X == not Y as it's likely an intentional boolean comparison
if (isNot(node->left) && !isNot(node->right))
{
std::string op = toString(node->op);
if (node->op == AstExprBinary::CompareEq || node->op == AstExprBinary::CompareNe)
emitWarning(*context, LintWarning::Code_ComparisonPrecedence, node->location,
"not X %s Y is equivalent to (not X) %s Y; consider using X %s Y, or wrap one of the expressions in parentheses to silence",
op.c_str(), op.c_str(), node->op == AstExprBinary::CompareEq ? "~=" : "==");
else
emitWarning(*context, LintWarning::Code_ComparisonPrecedence, node->location,
"not X %s Y is equivalent to (not X) %s Y; wrap one of the expressions in parentheses to silence", op.c_str(), op.c_str());
}
else if (AstExprBinary* left = node->left->as<AstExprBinary>(); left && isComparison(left->op))
{
std::string lop = toString(left->op);
std::string rop = toString(node->op);
emitWarning(*context, LintWarning::Code_ComparisonPrecedence, node->location,
"X %s Y %s Z is equivalent to (X %s Y) %s Z; wrap one of the expressions in parentheses to silence", lop.c_str(), rop.c_str(),
lop.c_str(), rop.c_str());
}
return true;
}
};
static void fillBuiltinGlobals(LintContext& context, const AstNameTable& names, const ScopePtr& env)
{
ScopePtr current = env;
@ -2871,6 +2932,9 @@ std::vector<LintWarning> lint(AstStat* root, const AstNameTable& names, const Sc
if (context.warningEnabled(LintWarning::Code_IntegerParsing))
LintIntegerParsing::process(context);
if (context.warningEnabled(LintWarning::Code_ComparisonPrecedence) && FFlag::LuauLintComparisonPrecedence)
LintComparisonPrecedence::process(context);
std::sort(context.result.begin(), context.result.end(), WarningComparator());
return context.result;

View file

@ -17,6 +17,10 @@
LUAU_FASTFLAG(LuauLowerBoundsCalculation);
LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution);
LUAU_FASTFLAGVARIABLE(LuauForceExportSurfacesToBeNormal, false);
LUAU_FASTFLAGVARIABLE(LuauClonePublicInterfaceLess, false);
LUAU_FASTFLAG(LuauSubstitutionReentrant);
LUAU_FASTFLAG(LuauClassTypeVarsInSubstitution);
LUAU_FASTFLAG(LuauSubstitutionFixMissingFields);
namespace Luau
{
@ -86,6 +90,118 @@ struct ForceNormal : TypeVarOnceVisitor
}
};
struct ClonePublicInterface : Substitution
{
NotNull<Module> module;
ClonePublicInterface(const TxnLog* log, Module* module)
: Substitution(log, &module->interfaceTypes)
, module(module)
{
LUAU_ASSERT(module);
}
bool isDirty(TypeId ty) override
{
if (ty->owningArena == &module->internalTypes)
return true;
if (const FunctionTypeVar* ftv = get<FunctionTypeVar>(ty))
return ftv->level.level != 0;
if (const TableTypeVar* ttv = get<TableTypeVar>(ty))
return ttv->level.level != 0;
return false;
}
bool isDirty(TypePackId tp) override
{
return tp->owningArena == &module->internalTypes;
}
TypeId clean(TypeId ty) override
{
TypeId result = clone(ty);
if (FunctionTypeVar* ftv = getMutable<FunctionTypeVar>(result))
ftv->level = TypeLevel{0, 0};
else if (TableTypeVar* ttv = getMutable<TableTypeVar>(result))
ttv->level = TypeLevel{0, 0};
return result;
}
TypePackId clean(TypePackId tp) override
{
return clone(tp);
}
TypeId cloneType(TypeId ty)
{
LUAU_ASSERT(FFlag::LuauSubstitutionReentrant && FFlag::LuauSubstitutionFixMissingFields);
std::optional<TypeId> result = substitute(ty);
if (result)
{
return *result;
}
else
{
module->errors.push_back(TypeError{module->scopes[0].first, UnificationTooComplex{}});
return getSingletonTypes().errorRecoveryType();
}
}
TypePackId cloneTypePack(TypePackId tp)
{
LUAU_ASSERT(FFlag::LuauSubstitutionReentrant && FFlag::LuauSubstitutionFixMissingFields);
std::optional<TypePackId> result = substitute(tp);
if (result)
{
return *result;
}
else
{
module->errors.push_back(TypeError{module->scopes[0].first, UnificationTooComplex{}});
return getSingletonTypes().errorRecoveryTypePack();
}
}
TypeFun cloneTypeFun(const TypeFun& tf)
{
LUAU_ASSERT(FFlag::LuauSubstitutionReentrant && FFlag::LuauSubstitutionFixMissingFields);
std::vector<GenericTypeDefinition> typeParams;
std::vector<GenericTypePackDefinition> typePackParams;
for (GenericTypeDefinition typeParam : tf.typeParams)
{
TypeId ty = cloneType(typeParam.ty);
std::optional<TypeId> defaultValue;
if (typeParam.defaultValue)
defaultValue = cloneType(*typeParam.defaultValue);
typeParams.push_back(GenericTypeDefinition{ty, defaultValue});
}
for (GenericTypePackDefinition typePackParam : tf.typePackParams)
{
TypePackId tp = cloneTypePack(typePackParam.tp);
std::optional<TypePackId> defaultValue;
if (typePackParam.defaultValue)
defaultValue = cloneTypePack(*typePackParam.defaultValue);
typePackParams.push_back(GenericTypePackDefinition{tp, defaultValue});
}
TypeId type = cloneType(tf.type);
return TypeFun{typeParams, typePackParams, type};
}
};
Module::~Module()
{
unfreeze(interfaceTypes);
@ -106,12 +222,21 @@ void Module::clonePublicInterface(InternalErrorReporter& ice)
std::unordered_map<Name, TypeFun>* exportedTypeBindings =
FFlag::DebugLuauDeferredConstraintResolution ? nullptr : &moduleScope->exportedTypeBindings;
returnType = clone(returnType, interfaceTypes, cloneState);
TxnLog log;
ClonePublicInterface clonePublicInterface{&log, this};
if (FFlag::LuauClonePublicInterfaceLess)
returnType = clonePublicInterface.cloneTypePack(returnType);
else
returnType = clone(returnType, interfaceTypes, cloneState);
moduleScope->returnType = returnType;
if (varargPack)
{
varargPack = clone(*varargPack, interfaceTypes, cloneState);
if (FFlag::LuauClonePublicInterfaceLess)
varargPack = clonePublicInterface.cloneTypePack(*varargPack);
else
varargPack = clone(*varargPack, interfaceTypes, cloneState);
moduleScope->varargPack = varargPack;
}
@ -119,12 +244,12 @@ void Module::clonePublicInterface(InternalErrorReporter& ice)
if (FFlag::LuauLowerBoundsCalculation)
{
normalize(returnType, interfaceTypes, ice);
normalize(returnType, NotNull{this}, ice);
if (FFlag::LuauForceExportSurfacesToBeNormal)
forceNormal.traverse(returnType);
if (varargPack)
{
normalize(*varargPack, interfaceTypes, ice);
normalize(*varargPack, NotNull{this}, ice);
if (FFlag::LuauForceExportSurfacesToBeNormal)
forceNormal.traverse(*varargPack);
}
@ -134,10 +259,13 @@ void Module::clonePublicInterface(InternalErrorReporter& ice)
{
for (auto& [name, tf] : *exportedTypeBindings)
{
tf = clone(tf, interfaceTypes, cloneState);
if (FFlag::LuauClonePublicInterfaceLess)
tf = clonePublicInterface.cloneTypeFun(tf);
else
tf = clone(tf, interfaceTypes, cloneState);
if (FFlag::LuauLowerBoundsCalculation)
{
normalize(tf.type, interfaceTypes, ice);
normalize(tf.type, NotNull{this}, ice);
// We're about to freeze the memory. We know that the flag is conservative by design. Cyclic tables
// won't be marked normal. If the types aren't normal by now, they never will be.
@ -148,7 +276,7 @@ void Module::clonePublicInterface(InternalErrorReporter& ice)
if (param.defaultValue)
{
normalize(*param.defaultValue, interfaceTypes, ice);
normalize(*param.defaultValue, NotNull{this}, ice);
forceNormal.traverse(*param.defaultValue);
}
}
@ -168,10 +296,13 @@ void Module::clonePublicInterface(InternalErrorReporter& ice)
for (auto& [name, ty] : declaredGlobals)
{
ty = clone(ty, interfaceTypes, cloneState);
if (FFlag::LuauClonePublicInterfaceLess)
ty = clonePublicInterface.cloneType(ty);
else
ty = clone(ty, interfaceTypes, cloneState);
if (FFlag::LuauLowerBoundsCalculation)
{
normalize(ty, interfaceTypes, ice);
normalize(ty, NotNull{this}, ice);
if (FFlag::LuauForceExportSurfacesToBeNormal)
forceNormal.traverse(ty);

View file

@ -15,7 +15,6 @@ LUAU_FASTINTVARIABLE(LuauNormalizeIterationLimit, 1200);
LUAU_FASTFLAGVARIABLE(LuauNormalizeCombineTableFix, false);
LUAU_FASTFLAGVARIABLE(LuauFixNormalizationOfCyclicUnions, false);
LUAU_FASTFLAG(LuauUnknownAndNeverType)
LUAU_FASTFLAG(LuauQuantifyConstrained)
namespace Luau
{
@ -55,11 +54,11 @@ struct Replacer
} // anonymous namespace
bool isSubtype(TypeId subTy, TypeId superTy, InternalErrorReporter& ice)
bool isSubtype(TypeId subTy, TypeId superTy, NotNull<Scope> scope, InternalErrorReporter& ice)
{
UnifierSharedState sharedState{&ice};
TypeArena arena;
Unifier u{&arena, Mode::Strict, Location{}, Covariant, sharedState};
Unifier u{&arena, Mode::Strict, scope, Location{}, Covariant, sharedState};
u.anyIsTop = true;
u.tryUnify(subTy, superTy);
@ -67,11 +66,11 @@ bool isSubtype(TypeId subTy, TypeId superTy, InternalErrorReporter& ice)
return ok;
}
bool isSubtype(TypePackId subPack, TypePackId superPack, InternalErrorReporter& ice)
bool isSubtype(TypePackId subPack, TypePackId superPack, NotNull<Scope> scope, InternalErrorReporter& ice)
{
UnifierSharedState sharedState{&ice};
TypeArena arena;
Unifier u{&arena, Mode::Strict, Location{}, Covariant, sharedState};
Unifier u{&arena, Mode::Strict, scope, Location{}, Covariant, sharedState};
u.anyIsTop = true;
u.tryUnify(subPack, superPack);
@ -134,13 +133,15 @@ struct Normalize final : TypeVarVisitor
{
using TypeVarVisitor::Set;
Normalize(TypeArena& arena, InternalErrorReporter& ice)
Normalize(TypeArena& arena, NotNull<Scope> scope, InternalErrorReporter& ice)
: arena(arena)
, scope(scope)
, ice(ice)
{
}
TypeArena& arena;
NotNull<Scope> scope;
InternalErrorReporter& ice;
int iterationLimit = 0;
@ -162,7 +163,8 @@ struct Normalize final : TypeVarVisitor
// It should never be the case that this TypeVar is normal, but is bound to a non-normal type, except in nontrivial cases.
LUAU_ASSERT(!ty->normal || ty->normal == btv.boundTo->normal);
asMutable(ty)->normal = btv.boundTo->normal;
if (!ty->normal)
asMutable(ty)->normal = btv.boundTo->normal;
return !ty->normal;
}
@ -214,22 +216,7 @@ struct Normalize final : TypeVarVisitor
traverse(part);
std::vector<TypeId> newParts = normalizeUnion(parts);
if (FFlag::LuauQuantifyConstrained)
{
ctv->parts = std::move(newParts);
}
else
{
const bool normal = areNormal(newParts, seen, ice);
if (newParts.size() == 1)
*asMutable(ty) = BoundTypeVar{newParts[0]};
else
*asMutable(ty) = UnionTypeVar{std::move(newParts)};
asMutable(ty)->normal = normal;
}
ctv->parts = std::move(newParts);
return false;
}
@ -287,12 +274,7 @@ struct Normalize final : TypeVarVisitor
}
// An unsealed table can never be normal, ditto for free tables iff the type it is bound to is also not normal.
if (FFlag::LuauQuantifyConstrained)
{
if (ttv.state == TableState::Generic || ttv.state == TableState::Sealed || (ttv.state == TableState::Free && follow(ty)->normal))
asMutable(ty)->normal = normal;
}
else
if (ttv.state == TableState::Generic || ttv.state == TableState::Sealed || (ttv.state == TableState::Free && follow(ty)->normal))
asMutable(ty)->normal = normal;
return false;
@ -517,9 +499,9 @@ struct Normalize final : TypeVarVisitor
for (TypeId& part : result)
{
if (isSubtype(ty, part, ice))
if (isSubtype(ty, part, scope, ice))
return; // no need to do anything
else if (isSubtype(part, ty, ice))
else if (isSubtype(part, ty, scope, ice))
{
part = ty; // replace the less general type by the more general one
return;
@ -571,12 +553,12 @@ struct Normalize final : TypeVarVisitor
bool merged = false;
for (TypeId& part : result->parts)
{
if (isSubtype(part, ty, ice))
if (isSubtype(part, ty, scope, ice))
{
merged = true;
break; // no need to do anything
}
else if (isSubtype(ty, part, ice))
else if (isSubtype(ty, part, scope, ice))
{
merged = true;
part = ty; // replace the less general type by the more general one
@ -709,13 +691,13 @@ struct Normalize final : TypeVarVisitor
/**
* @returns A tuple of TypeId and a success indicator. (true indicates that the normalization completed successfully)
*/
std::pair<TypeId, bool> normalize(TypeId ty, TypeArena& arena, InternalErrorReporter& ice)
std::pair<TypeId, bool> normalize(TypeId ty, NotNull<Scope> scope, TypeArena& arena, InternalErrorReporter& ice)
{
CloneState state;
if (FFlag::DebugLuauCopyBeforeNormalizing)
(void)clone(ty, arena, state);
Normalize n{arena, ice};
Normalize n{arena, scope, ice};
n.traverse(ty);
return {ty, !n.limitExceeded};
@ -725,29 +707,39 @@ std::pair<TypeId, bool> normalize(TypeId ty, TypeArena& arena, InternalErrorRepo
// reclaim memory used by wantonly allocated intermediate types here.
// The main wrinkle here is that we don't want clone() to copy a type if the source and dest
// arena are the same.
std::pair<TypeId, bool> normalize(TypeId ty, NotNull<Module> module, InternalErrorReporter& ice)
{
return normalize(ty, NotNull{module->getModuleScope().get()}, module->internalTypes, ice);
}
std::pair<TypeId, bool> normalize(TypeId ty, const ModulePtr& module, InternalErrorReporter& ice)
{
return normalize(ty, module->internalTypes, ice);
return normalize(ty, NotNull{module.get()}, ice);
}
/**
* @returns A tuple of TypeId and a success indicator. (true indicates that the normalization completed successfully)
*/
std::pair<TypePackId, bool> normalize(TypePackId tp, TypeArena& arena, InternalErrorReporter& ice)
std::pair<TypePackId, bool> normalize(TypePackId tp, NotNull<Scope> scope, TypeArena& arena, InternalErrorReporter& ice)
{
CloneState state;
if (FFlag::DebugLuauCopyBeforeNormalizing)
(void)clone(tp, arena, state);
Normalize n{arena, ice};
Normalize n{arena, scope, ice};
n.traverse(tp);
return {tp, !n.limitExceeded};
}
std::pair<TypePackId, bool> normalize(TypePackId tp, NotNull<Module> module, InternalErrorReporter& ice)
{
return normalize(tp, NotNull{module->getModuleScope().get()}, module->internalTypes, ice);
}
std::pair<TypePackId, bool> normalize(TypePackId tp, const ModulePtr& module, InternalErrorReporter& ice)
{
return normalize(tp, module->internalTypes, ice);
return normalize(tp, NotNull{module.get()}, ice);
}
} // namespace Luau

View file

@ -5,11 +5,12 @@
#include "Luau/Scope.h"
#include "Luau/Substitution.h"
#include "Luau/TxnLog.h"
#include "Luau/TypeVar.h"
#include "Luau/VisitTypeVar.h"
LUAU_FASTFLAG(DebugLuauSharedSelf)
LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution);
LUAU_FASTFLAGVARIABLE(LuauQuantifyConstrained, false)
LUAU_FASTFLAG(LuauClassTypeVarsInSubstitution)
namespace Luau
{
@ -80,30 +81,25 @@ struct Quantifier final : TypeVarOnceVisitor
bool visit(TypeId ty, const ConstrainedTypeVar&) override
{
if (FFlag::LuauQuantifyConstrained)
{
ConstrainedTypeVar* ctv = getMutable<ConstrainedTypeVar>(ty);
ConstrainedTypeVar* ctv = getMutable<ConstrainedTypeVar>(ty);
seenMutableType = true;
if (FFlag::DebugLuauDeferredConstraintResolution ? !subsumes(scope, ctv->scope) : !level.subsumes(ctv->level))
return false;
std::vector<TypeId> opts = std::move(ctv->parts);
// We might transmute, so it's not safe to rely on the builtin traversal logic
for (TypeId opt : opts)
traverse(opt);
if (opts.size() == 1)
*asMutable(ty) = BoundTypeVar{opts[0]};
else
*asMutable(ty) = UnionTypeVar{std::move(opts)};
seenMutableType = true;
if (FFlag::DebugLuauDeferredConstraintResolution ? !subsumes(scope, ctv->scope) : !level.subsumes(ctv->level))
return false;
}
std::vector<TypeId> opts = std::move(ctv->parts);
// We might transmute, so it's not safe to rely on the builtin traversal logic
for (TypeId opt : opts)
traverse(opt);
if (opts.size() == 1)
*asMutable(ty) = BoundTypeVar{opts[0]};
else
return true;
*asMutable(ty) = UnionTypeVar{std::move(opts)};
return false;
}
bool visit(TypeId ty, const TableTypeVar&) override
@ -117,12 +113,6 @@ struct Quantifier final : TypeVarOnceVisitor
if (ttv.state == TableState::Free)
seenMutableType = true;
if (!FFlag::LuauQuantifyConstrained)
{
if (ttv.state == TableState::Sealed || ttv.state == TableState::Generic)
return false;
}
if (FFlag::DebugLuauDeferredConstraintResolution ? !subsumes(scope, ttv.scope) : !level.subsumes(ttv.level))
{
if (ttv.state == TableState::Unsealed)
@ -297,6 +287,9 @@ struct PureQuantifier : Substitution
bool ignoreChildren(TypeId ty) override
{
if (FFlag::LuauClassTypeVarsInSubstitution && get<ClassTypeVar>(ty))
return true;
return ty->persistent;
}
bool ignoreChildren(TypePackId ty) override

View file

@ -122,34 +122,4 @@ std::optional<TypeId> Scope::lookup(Symbol sym)
}
}
std::optional<TypeFun> Scope::lookupTypeBinding(const Name& name)
{
Scope* s = this;
while (s)
{
auto it = s->typeBindings.find(name);
if (it != s->typeBindings.end())
return it->second;
s = s->parent.get();
}
return std::nullopt;
}
std::optional<TypePackId> Scope::lookupTypePackBinding(const Name& name)
{
Scope* s = this;
while (s)
{
auto it = s->typePackBindings.find(name);
if (it != s->typePackBindings.end())
return it->second;
s = s->parent.get();
}
return std::nullopt;
}
} // namespace Luau

View file

@ -8,9 +8,9 @@
#include <algorithm>
#include <stdexcept>
LUAU_FASTFLAGVARIABLE(LuauAnyificationMustClone, false)
LUAU_FASTFLAGVARIABLE(LuauSubstitutionFixMissingFields, false)
LUAU_FASTFLAG(LuauLowerBoundsCalculation)
LUAU_FASTFLAG(LuauClonePublicInterfaceLess)
LUAU_FASTINTVARIABLE(LuauTarjanChildLimit, 10000)
LUAU_FASTFLAGVARIABLE(LuauClassTypeVarsInSubstitution, false)
LUAU_FASTFLAG(LuauUnknownAndNeverType)
@ -472,7 +472,7 @@ std::optional<TypePackId> Substitution::substitute(TypePackId tp)
TypeId Substitution::clone(TypeId ty)
{
return shallowClone(ty, *arena, log);
return shallowClone(ty, *arena, log, /* alwaysClone */ FFlag::LuauClonePublicInterfaceLess);
}
TypePackId Substitution::clone(TypePackId tp)
@ -497,6 +497,10 @@ TypePackId Substitution::clone(TypePackId tp)
clone.hidden = vtp->hidden;
return addTypePack(std::move(clone));
}
else if (FFlag::LuauClonePublicInterfaceLess)
{
return addTypePack(*tp);
}
else
return tp;
}
@ -557,7 +561,7 @@ void Substitution::replaceChildren(TypeId ty)
if (ignoreChildren(ty))
return;
if (FFlag::LuauAnyificationMustClone && ty->owningArena != arena)
if (ty->owningArena != arena)
return;
if (FunctionTypeVar* ftv = getMutable<FunctionTypeVar>(ty))
@ -638,7 +642,7 @@ void Substitution::replaceChildren(TypePackId tp)
if (ignoreChildren(tp))
return;
if (FFlag::LuauAnyificationMustClone && tp->owningArena != arena)
if (tp->owningArena != arena)
return;
if (TypePack* tpp = getMutable<TypePack>(tp))

View file

@ -8,21 +8,59 @@
#include "Luau/Clone.h"
#include "Luau/Instantiation.h"
#include "Luau/Normalize.h"
#include "Luau/ToString.h"
#include "Luau/TxnLog.h"
#include "Luau/TypeUtils.h"
#include "Luau/TypeVar.h"
#include "Luau/Unifier.h"
#include "Luau/ToString.h"
namespace Luau
{
struct TypeChecker2 : public AstVisitor
/* Push a scope onto the end of a stack for the lifetime of the StackPusher instance.
* TypeChecker2 uses this to maintain knowledge about which scope encloses every
* given AstNode.
*/
struct StackPusher
{
std::vector<NotNull<Scope>>* stack;
NotNull<Scope> scope;
explicit StackPusher(std::vector<NotNull<Scope>>& stack, Scope* scope)
: stack(&stack)
, scope(scope)
{
stack.push_back(NotNull{scope});
}
~StackPusher()
{
if (stack)
{
LUAU_ASSERT(stack->back() == scope);
stack->pop_back();
}
}
StackPusher(const StackPusher&) = delete;
StackPusher&& operator=(const StackPusher&) = delete;
StackPusher(StackPusher&& other)
: stack(std::exchange(other.stack, nullptr))
, scope(other.scope)
{
}
};
struct TypeChecker2
{
const SourceModule* sourceModule;
Module* module;
InternalErrorReporter ice; // FIXME accept a pointer from Frontend
SingletonTypes& singletonTypes;
std::vector<NotNull<Scope>> stack;
TypeChecker2(const SourceModule* sourceModule, Module* module)
: sourceModule(sourceModule)
, module(module)
@ -30,7 +68,13 @@ struct TypeChecker2 : public AstVisitor
{
}
using AstVisitor::visit;
std::optional<StackPusher> pushStack(AstNode* node)
{
if (Scope** scope = module->astScopes.find(node))
return StackPusher{stack, *scope};
else
return std::nullopt;
}
TypePackId lookupPack(AstExpr* expr)
{
@ -117,11 +161,128 @@ struct TypeChecker2 : public AstVisitor
return bestScope;
}
bool visit(AstStatLocal* local) override
void visit(AstStat* stat)
{
auto pusher = pushStack(stat);
if (0)
{}
else if (auto s = stat->as<AstStatBlock>())
return visit(s);
else if (auto s = stat->as<AstStatIf>())
return visit(s);
else if (auto s = stat->as<AstStatWhile>())
return visit(s);
else if (auto s = stat->as<AstStatRepeat>())
return visit(s);
else if (auto s = stat->as<AstStatBreak>())
return visit(s);
else if (auto s = stat->as<AstStatContinue>())
return visit(s);
else if (auto s = stat->as<AstStatReturn>())
return visit(s);
else if (auto s = stat->as<AstStatExpr>())
return visit(s);
else if (auto s = stat->as<AstStatLocal>())
return visit(s);
else if (auto s = stat->as<AstStatFor>())
return visit(s);
else if (auto s = stat->as<AstStatForIn>())
return visit(s);
else if (auto s = stat->as<AstStatAssign>())
return visit(s);
else if (auto s = stat->as<AstStatCompoundAssign>())
return visit(s);
else if (auto s = stat->as<AstStatFunction>())
return visit(s);
else if (auto s = stat->as<AstStatLocalFunction>())
return visit(s);
else if (auto s = stat->as<AstStatTypeAlias>())
return visit(s);
else if (auto s = stat->as<AstStatDeclareFunction>())
return visit(s);
else if (auto s = stat->as<AstStatDeclareGlobal>())
return visit(s);
else if (auto s = stat->as<AstStatDeclareClass>())
return visit(s);
else if (auto s = stat->as<AstStatError>())
return visit(s);
else
LUAU_ASSERT(!"TypeChecker2 encountered an unknown node type");
}
void visit(AstStatBlock* block)
{
auto StackPusher = pushStack(block);
for (AstStat* statement : block->body)
visit(statement);
}
void visit(AstStatIf* ifStatement)
{
visit(ifStatement->condition);
visit(ifStatement->thenbody);
if (ifStatement->elsebody)
visit(ifStatement->elsebody);
}
void visit(AstStatWhile* whileStatement)
{
visit(whileStatement->condition);
visit(whileStatement->body);
}
void visit(AstStatRepeat* repeatStatement)
{
visit(repeatStatement->body);
visit(repeatStatement->condition);
}
void visit(AstStatBreak*)
{}
void visit(AstStatContinue*)
{}
void visit(AstStatReturn* ret)
{
Scope* scope = findInnermostScope(ret->location);
TypePackId expectedRetType = scope->returnType;
TypeArena arena;
TypePackId actualRetType = reconstructPack(ret->list, arena);
UnifierSharedState sharedState{&ice};
Unifier u{&arena, Mode::Strict, stack.back(), ret->location, Covariant, sharedState};
u.anyIsTop = true;
u.tryUnify(actualRetType, expectedRetType);
const bool ok = u.errors.empty() && u.log.empty();
if (!ok)
{
for (const TypeError& e : u.errors)
reportError(e);
}
for (AstExpr* expr : ret->list)
visit(expr);
}
void visit(AstStatExpr* expr)
{
visit(expr->expr);
}
void visit(AstStatLocal* local)
{
for (size_t i = 0; i < local->values.size; ++i)
{
AstExpr* value = local->values.data[i];
visit(value);
if (i == local->values.size - 1)
{
if (i < local->values.size)
@ -139,7 +300,7 @@ struct TypeChecker2 : public AstVisitor
if (var->annotation)
{
TypeId varType = lookupAnnotation(var->annotation);
if (!isSubtype(*it, varType, ice))
if (!isSubtype(*it, varType, stack.back(), ice))
{
reportError(TypeMismatch{varType, *it}, value->location);
}
@ -157,64 +318,244 @@ struct TypeChecker2 : public AstVisitor
if (var->annotation)
{
TypeId varType = lookupAnnotation(var->annotation);
if (!isSubtype(varType, valueType, ice))
if (!isSubtype(varType, valueType, stack.back(), ice))
{
reportError(TypeMismatch{varType, valueType}, value->location);
}
}
}
}
return true;
}
bool visit(AstStatAssign* assign) override
void visit(AstStatFor* forStatement)
{
if (forStatement->var->annotation)
visit(forStatement->var->annotation);
visit(forStatement->from);
visit(forStatement->to);
if (forStatement->step)
visit(forStatement->step);
visit(forStatement->body);
}
void visit(AstStatForIn* forInStatement)
{
for (AstLocal* local : forInStatement->vars)
{
if (local->annotation)
visit(local->annotation);
}
for (AstExpr* expr : forInStatement->values)
visit(expr);
visit(forInStatement->body);
}
void visit(AstStatAssign* assign)
{
size_t count = std::min(assign->vars.size, assign->values.size);
for (size_t i = 0; i < count; ++i)
{
AstExpr* lhs = assign->vars.data[i];
visit(lhs);
TypeId lhsType = lookupType(lhs);
AstExpr* rhs = assign->values.data[i];
visit(rhs);
TypeId rhsType = lookupType(rhs);
if (!isSubtype(rhsType, lhsType, ice))
if (!isSubtype(rhsType, lhsType, stack.back(), ice))
{
reportError(TypeMismatch{lhsType, rhsType}, rhs->location);
}
}
return true;
}
bool visit(AstStatReturn* ret) override
void visit(AstStatCompoundAssign* stat)
{
Scope* scope = findInnermostScope(ret->location);
TypePackId expectedRetType = scope->returnType;
visit(stat->var);
visit(stat->value);
}
TypeArena arena;
TypePackId actualRetType = reconstructPack(ret->list, arena);
void visit(AstStatFunction* stat)
{
visit(stat->name);
visit(stat->func);
}
UnifierSharedState sharedState{&ice};
Unifier u{&arena, Mode::Strict, ret->location, Covariant, sharedState};
u.anyIsTop = true;
void visit(AstStatLocalFunction* stat)
{
visit(stat->func);
}
u.tryUnify(actualRetType, expectedRetType);
const bool ok = u.errors.empty() && u.log.empty();
void visit(const AstTypeList* typeList)
{
for (AstType* ty : typeList->types)
visit(ty);
if (!ok)
if (typeList->tailType)
visit(typeList->tailType);
}
void visit(AstStatTypeAlias* stat)
{
for (const AstGenericType& el : stat->generics)
{
for (const TypeError& e : u.errors)
reportError(e);
if (el.defaultValue)
visit(el.defaultValue);
}
return true;
for (const AstGenericTypePack& el : stat->genericPacks)
{
if (el.defaultValue)
visit(el.defaultValue);
}
visit(stat->type);
}
bool visit(AstExprCall* call) override
void visit(AstTypeList types)
{
for (AstType* type : types.types)
visit(type);
if (types.tailType)
visit(types.tailType);
}
void visit(AstStatDeclareFunction* stat)
{
visit(stat->params);
visit(stat->retTypes);
}
void visit(AstStatDeclareGlobal* stat)
{
visit(stat->type);
}
void visit(AstStatDeclareClass* stat)
{
for (const AstDeclaredClassProp& prop : stat->props)
visit(prop.ty);
}
void visit(AstStatError* stat)
{
for (AstExpr* expr : stat->expressions)
visit(expr);
for (AstStat* s : stat->statements)
visit(s);
}
void visit(AstExpr* expr)
{
auto StackPusher = pushStack(expr);
if (0)
{}
else if (auto e = expr->as<AstExprGroup>())
return visit(e);
else if (auto e = expr->as<AstExprConstantNil>())
return visit(e);
else if (auto e = expr->as<AstExprConstantBool>())
return visit(e);
else if (auto e = expr->as<AstExprConstantNumber>())
return visit(e);
else if (auto e = expr->as<AstExprConstantString>())
return visit(e);
else if (auto e = expr->as<AstExprLocal>())
return visit(e);
else if (auto e = expr->as<AstExprGlobal>())
return visit(e);
else if (auto e = expr->as<AstExprVarargs>())
return visit(e);
else if (auto e = expr->as<AstExprCall>())
return visit(e);
else if (auto e = expr->as<AstExprIndexName>())
return visit(e);
else if (auto e = expr->as<AstExprIndexExpr>())
return visit(e);
else if (auto e = expr->as<AstExprFunction>())
return visit(e);
else if (auto e = expr->as<AstExprTable>())
return visit(e);
else if (auto e = expr->as<AstExprUnary>())
return visit(e);
else if (auto e = expr->as<AstExprBinary>())
return visit(e);
else if (auto e = expr->as<AstExprTypeAssertion>())
return visit(e);
else if (auto e = expr->as<AstExprIfElse>())
return visit(e);
else if (auto e = expr->as<AstExprError>())
return visit(e);
else
LUAU_ASSERT(!"TypeChecker2 encountered an unknown expression type");
}
void visit(AstExprGroup* expr)
{
visit(expr->expr);
}
void visit(AstExprConstantNil* expr)
{
// TODO!
}
void visit(AstExprConstantBool* expr)
{
// TODO!
}
void visit(AstExprConstantNumber* number)
{
TypeId actualType = lookupType(number);
TypeId numberType = getSingletonTypes().numberType;
if (!isSubtype(numberType, actualType, stack.back(), ice))
{
reportError(TypeMismatch{actualType, numberType}, number->location);
}
}
void visit(AstExprConstantString* string)
{
TypeId actualType = lookupType(string);
TypeId stringType = getSingletonTypes().stringType;
if (!isSubtype(stringType, actualType, stack.back(), ice))
{
reportError(TypeMismatch{actualType, stringType}, string->location);
}
}
void visit(AstExprLocal* expr)
{
// TODO!
}
void visit(AstExprGlobal* expr)
{
// TODO!
}
void visit(AstExprVarargs* expr)
{
// TODO!
}
void visit(AstExprCall* call)
{
visit(call->func);
for (AstExpr* arg : call->args)
visit(arg);
TypeArena arena;
Instantiation instantiation{TxnLog::empty(), &arena, TypeLevel{}};
@ -224,7 +565,7 @@ struct TypeChecker2 : public AstVisitor
LUAU_ASSERT(functionType);
TypePack args;
for (const auto& arg : call->args)
for (AstExpr* arg : call->args)
{
TypeId argTy = module->astTypes[arg];
LUAU_ASSERT(argTy);
@ -234,7 +575,7 @@ struct TypeChecker2 : public AstVisitor
TypePackId argsTp = arena.addTypePack(args);
FunctionTypeVar ftv{argsTp, expectedRetType};
TypeId expectedType = arena.addType(ftv);
if (!isSubtype(expectedType, instantiatedFunctionType, ice))
if (!isSubtype(expectedType, instantiatedFunctionType, stack.back(), ice))
{
unfreeze(module->interfaceTypes);
CloneState cloneState;
@ -242,12 +583,36 @@ struct TypeChecker2 : public AstVisitor
freeze(module->interfaceTypes);
reportError(TypeMismatch{expectedType, functionType}, call->location);
}
return true;
}
bool visit(AstExprFunction* fn) override
void visit(AstExprIndexName* indexName)
{
TypeId leftType = lookupType(indexName->expr);
TypeId resultType = lookupType(indexName);
// leftType must have a property called indexName->index
std::optional<TypeId> ty = getIndexTypeFromType(module->getModuleScope(), leftType, indexName->index.value, indexName->location, /* addErrors */ true);
if (ty)
{
if (!isSubtype(resultType, *ty, stack.back(), ice))
{
reportError(TypeMismatch{resultType, *ty}, indexName->location);
}
}
}
void visit(AstExprIndexExpr* indexExpr)
{
// TODO!
visit(indexExpr->expr);
visit(indexExpr->index);
}
void visit(AstExprFunction* fn)
{
auto StackPusher = pushStack(fn);
TypeId inferredFnTy = lookupType(fn);
const FunctionTypeVar* inferredFtv = get<FunctionTypeVar>(inferredFnTy);
LUAU_ASSERT(inferredFtv);
@ -263,7 +628,7 @@ struct TypeChecker2 : public AstVisitor
TypeId inferredArgTy = *argIt;
TypeId annotatedArgTy = lookupAnnotation(arg->annotation);
if (!isSubtype(annotatedArgTy, inferredArgTy, ice))
if (!isSubtype(annotatedArgTy, inferredArgTy, stack.back(), ice))
{
reportError(TypeMismatch{annotatedArgTy, inferredArgTy}, arg->location);
}
@ -272,56 +637,64 @@ struct TypeChecker2 : public AstVisitor
++argIt;
}
return true;
visit(fn->body);
}
bool visit(AstExprIndexName* indexName) override
void visit(AstExprTable* expr)
{
TypeId leftType = lookupType(indexName->expr);
TypeId resultType = lookupType(indexName);
// leftType must have a property called indexName->index
std::optional<TypeId> t = findTablePropertyRespectingMeta(module->errors, leftType, indexName->index.value, indexName->location);
if (t)
// TODO!
for (const AstExprTable::Item& item : expr->items)
{
if (!isSubtype(resultType, *t, ice))
{
reportError(TypeMismatch{resultType, *t}, indexName->location);
}
if (item.key)
visit(item.key);
visit(item.value);
}
else
{
reportError(UnknownProperty{leftType, indexName->index.value}, indexName->location);
}
return true;
}
bool visit(AstExprConstantNumber* number) override
void visit(AstExprUnary* expr)
{
TypeId actualType = lookupType(number);
TypeId numberType = getSingletonTypes().numberType;
if (!isSubtype(numberType, actualType, ice))
{
reportError(TypeMismatch{actualType, numberType}, number->location);
}
return true;
// TODO!
visit(expr->expr);
}
bool visit(AstExprConstantString* string) override
void visit(AstExprBinary* expr)
{
TypeId actualType = lookupType(string);
TypeId stringType = getSingletonTypes().stringType;
// TODO!
visit(expr->left);
visit(expr->right);
}
if (!isSubtype(stringType, actualType, ice))
{
reportError(TypeMismatch{actualType, stringType}, string->location);
}
void visit(AstExprTypeAssertion* expr)
{
visit(expr->expr);
visit(expr->annotation);
return true;
TypeId annotationType = lookupAnnotation(expr->annotation);
TypeId computedType = lookupType(expr->expr);
// Note: As an optimization, we try 'number <: number | string' first, as that is the more likely case.
if (isSubtype(annotationType, computedType, stack.back(), ice))
return;
if (isSubtype(computedType, annotationType, stack.back(), ice))
return;
reportError(TypesAreUnrelated{computedType, annotationType}, expr->location);
}
void visit(AstExprIfElse* expr)
{
// TODO!
visit(expr->condition);
visit(expr->trueExpr);
visit(expr->falseExpr);
}
void visit(AstExprError* expr)
{
// TODO!
for (AstExpr* e : expr->expressions)
visit(e);
}
/** Extract a TypeId for the first type of the provided pack.
@ -362,19 +735,38 @@ struct TypeChecker2 : public AstVisitor
ice.ice("flattenPack got a weird pack!");
}
bool visit(AstType* ty) override
void visit(AstType* ty)
{
return true;
if (auto t = ty->as<AstTypeReference>())
return visit(t);
else if (auto t = ty->as<AstTypeTable>())
return visit(t);
else if (auto t = ty->as<AstTypeFunction>())
return visit(t);
else if (auto t = ty->as<AstTypeTypeof>())
return visit(t);
else if (auto t = ty->as<AstTypeUnion>())
return visit(t);
else if (auto t = ty->as<AstTypeIntersection>())
return visit(t);
}
bool visit(AstTypeReference* ty) override
void visit(AstTypeReference* ty)
{
for (const AstTypeOrPack& param : ty->parameters)
{
if (param.type)
visit(param.type);
else
visit(param.typePack);
}
Scope* scope = findInnermostScope(ty->location);
LUAU_ASSERT(scope);
// TODO: Imported types
std::optional<TypeFun> alias = scope->lookupTypeBinding(ty->name.value);
std::optional<TypeFun> alias = scope->lookupType(ty->name.value);
if (alias.has_value())
{
@ -473,7 +865,7 @@ struct TypeChecker2 : public AstVisitor
}
else
{
if (scope->lookupTypePackBinding(ty->name.value))
if (scope->lookupPack(ty->name.value))
{
reportError(
SwappedGenericTypeParameter{
@ -487,24 +879,84 @@ struct TypeChecker2 : public AstVisitor
reportError(UnknownSymbol{ty->name.value, UnknownSymbol::Context::Type}, ty->location);
}
}
return true;
}
bool visit(AstTypePack*) override
void visit(AstTypeTable* table)
{
return true;
// TODO!
for (const AstTableProp& prop : table->props)
visit(prop.type);
if (table->indexer)
{
visit(table->indexer->indexType);
visit(table->indexer->resultType);
}
}
bool visit(AstTypePackGeneric* tp) override
void visit(AstTypeFunction* ty)
{
// TODO!
visit(ty->argTypes);
visit(ty->returnTypes);
}
void visit(AstTypeTypeof* ty)
{
visit(ty->expr);
}
void visit(AstTypeUnion* ty)
{
// TODO!
for (AstType* type : ty->types)
visit(type);
}
void visit(AstTypeIntersection* ty)
{
// TODO!
for (AstType* type : ty->types)
visit(type);
}
void visit(AstTypePack* pack)
{
if (auto p = pack->as<AstTypePackExplicit>())
return visit(p);
else if (auto p = pack->as<AstTypePackVariadic>())
return visit(p);
else if (auto p = pack->as<AstTypePackGeneric>())
return visit(p);
}
void visit(AstTypePackExplicit* tp)
{
// TODO!
for (AstType* type : tp->typeList.types)
visit(type);
if (tp->typeList.tailType)
visit(tp->typeList.tailType);
}
void visit(AstTypePackVariadic* tp)
{
// TODO!
visit(tp->variadicType);
}
void visit(AstTypePackGeneric* tp)
{
Scope* scope = findInnermostScope(tp->location);
LUAU_ASSERT(scope);
std::optional<TypePackId> alias = scope->lookupTypePackBinding(tp->genericName.value);
std::optional<TypePackId> alias = scope->lookupPack(tp->genericName.value);
if (!alias.has_value())
{
if (scope->lookupTypeBinding(tp->genericName.value))
if (scope->lookupType(tp->genericName.value))
{
reportError(
SwappedGenericTypeParameter{
@ -518,8 +970,6 @@ struct TypeChecker2 : public AstVisitor
reportError(UnknownSymbol{tp->genericName.value, UnknownSymbol::Context::Type}, tp->location);
}
}
return true;
}
void reportError(TypeErrorData&& data, const Location& location)
@ -531,13 +981,19 @@ struct TypeChecker2 : public AstVisitor
{
module->errors.emplace_back(std::move(e));
}
std::optional<TypeId> getIndexTypeFromType(
const ScopePtr& scope, TypeId type, const std::string& prop, const Location& location, bool addErrors)
{
return Luau::getIndexTypeFromType(scope, module->errors, &module->internalTypes, type, prop, location, addErrors, ice);
}
};
void check(const SourceModule& sourceModule, Module* module)
{
TypeChecker2 typeChecker{&sourceModule, module};
sourceModule.root->visit(&typeChecker);
typeChecker.visit(sourceModule.root);
}
} // namespace Luau

View file

@ -33,16 +33,13 @@ LUAU_FASTINTVARIABLE(LuauVisitRecursionLimit, 500)
LUAU_FASTFLAG(LuauKnowsTheDataModel3)
LUAU_FASTFLAG(LuauAutocompleteDynamicLimits)
LUAU_FASTFLAGVARIABLE(LuauExpectedTableUnionIndexerType, false)
LUAU_FASTFLAGVARIABLE(LuauIndexSilenceErrors, false)
LUAU_FASTFLAGVARIABLE(LuauInplaceDemoteSkipAllBound, false)
LUAU_FASTFLAGVARIABLE(LuauLowerBoundsCalculation, false)
LUAU_FASTFLAGVARIABLE(DebugLuauFreezeDuringUnification, false)
LUAU_FASTFLAGVARIABLE(LuauSelfCallAutocompleteFix3, false)
LUAU_FASTFLAGVARIABLE(LuauReturnAnyInsteadOfICE, false) // Eventually removed as false.
LUAU_FASTFLAGVARIABLE(DebugLuauSharedSelf, false);
LUAU_FASTFLAGVARIABLE(LuauReportErrorsOnIndexerKeyMismatch, false)
LUAU_FASTFLAGVARIABLE(LuauUnknownAndNeverType, false)
LUAU_FASTFLAG(LuauQuantifyConstrained)
LUAU_FASTFLAGVARIABLE(LuauFalsyPredicateReturnsNilInstead, false)
LUAU_FASTFLAGVARIABLE(LuauCheckGenericHOFTypes, false)
LUAU_FASTFLAGVARIABLE(LuauBinaryNeedsExpectedTypesToo, false)
LUAU_FASTFLAGVARIABLE(LuauNeverTypesAndOperatorsInference, false)
@ -474,7 +471,8 @@ struct InplaceDemoter : TypeVarOnceVisitor
TypeArena* arena;
InplaceDemoter(TypeLevel level, TypeArena* arena)
: newLevel(level)
: TypeVarOnceVisitor(/* skipBoundTypes= */ FFlag::LuauInplaceDemoteSkipAllBound)
, newLevel(level)
, arena(arena)
{
}
@ -495,6 +493,7 @@ struct InplaceDemoter : TypeVarOnceVisitor
bool visit(TypeId ty, const BoundTypeVar& btyRef) override
{
LUAU_ASSERT(!FFlag::LuauInplaceDemoteSkipAllBound);
return true;
}
@ -657,7 +656,7 @@ void TypeChecker::checkBlockWithoutRecursionCheck(const ScopePtr& scope, const A
TypeId leftType = follow(checkFunctionName(scope, *fun->name, funScope->level));
unify(funTy, leftType, fun->location);
unify(funTy, leftType, scope, fun->location);
}
else if (auto fun = (*protoIter)->as<AstStatLocalFunction>())
{
@ -769,20 +768,20 @@ void TypeChecker::check(const ScopePtr& scope, const AstStatIf& statement)
}
template<typename Id>
ErrorVec TypeChecker::canUnify_(Id subTy, Id superTy, const Location& location)
ErrorVec TypeChecker::canUnify_(Id subTy, Id superTy, const ScopePtr& scope, const Location& location)
{
Unifier state = mkUnifier(location);
Unifier state = mkUnifier(scope, location);
return state.canUnify(subTy, superTy);
}
ErrorVec TypeChecker::canUnify(TypeId subTy, TypeId superTy, const Location& location)
ErrorVec TypeChecker::canUnify(TypeId subTy, TypeId superTy, const ScopePtr& scope, const Location& location)
{
return canUnify_(subTy, superTy, location);
return canUnify_(subTy, superTy, scope, location);
}
ErrorVec TypeChecker::canUnify(TypePackId subTy, TypePackId superTy, const Location& location)
ErrorVec TypeChecker::canUnify(TypePackId subTy, TypePackId superTy, const ScopePtr& scope, const Location& location)
{
return canUnify_(subTy, superTy, location);
return canUnify_(subTy, superTy, scope, location);
}
void TypeChecker::check(const ScopePtr& scope, const AstStatWhile& statement)
@ -803,9 +802,9 @@ void TypeChecker::check(const ScopePtr& scope, const AstStatRepeat& statement)
checkExpr(repScope, *statement.condition);
}
void TypeChecker::unifyLowerBound(TypePackId subTy, TypePackId superTy, TypeLevel demotedLevel, const Location& location)
void TypeChecker::unifyLowerBound(TypePackId subTy, TypePackId superTy, TypeLevel demotedLevel, const ScopePtr& scope, const Location& location)
{
Unifier state = mkUnifier(location);
Unifier state = mkUnifier(scope, location);
state.unifyLowerBound(subTy, superTy, demotedLevel);
state.log.commit();
@ -830,6 +829,14 @@ struct Demoter : Substitution
return get<FreeTypePack>(tp);
}
bool ignoreChildren(TypeId ty) override
{
if (FFlag::LuauClassTypeVarsInSubstitution && get<ClassTypeVar>(ty))
return true;
return false;
}
TypeId clean(TypeId ty) override
{
auto ftv = get<FreeTypeVar>(ty);
@ -851,8 +858,6 @@ struct Demoter : Substitution
void demote(std::vector<std::optional<TypeId>>& expectedTypes)
{
if (!FFlag::LuauQuantifyConstrained)
return;
for (std::optional<TypeId>& ty : expectedTypes)
{
if (ty)
@ -890,7 +895,7 @@ void TypeChecker::check(const ScopePtr& scope, const AstStatReturn& return_)
if (useConstrainedIntersections())
{
unifyLowerBound(retPack, scope->returnType, demoter.demotedLevel(scope->level), return_.location);
unifyLowerBound(retPack, scope->returnType, demoter.demotedLevel(scope->level), scope, return_.location);
return;
}
@ -898,7 +903,7 @@ void TypeChecker::check(const ScopePtr& scope, const AstStatReturn& return_)
// start typechecking everything across module boundaries.
if (isNonstrictMode() && follow(scope->returnType) == follow(currentModule->getModuleScope()->returnType))
{
ErrorVec errors = tryUnify(retPack, scope->returnType, return_.location);
ErrorVec errors = tryUnify(retPack, scope->returnType, scope, return_.location);
if (!errors.empty())
currentModule->getModuleScope()->returnType = addTypePack({anyType});
@ -906,13 +911,13 @@ void TypeChecker::check(const ScopePtr& scope, const AstStatReturn& return_)
return;
}
unify(retPack, scope->returnType, return_.location, CountMismatch::Context::Return);
unify(retPack, scope->returnType, scope, return_.location, CountMismatch::Context::Return);
}
template<typename Id>
ErrorVec TypeChecker::tryUnify_(Id subTy, Id superTy, const Location& location)
ErrorVec TypeChecker::tryUnify_(Id subTy, Id superTy, const ScopePtr& scope, const Location& location)
{
Unifier state = mkUnifier(location);
Unifier state = mkUnifier(scope, location);
if (FFlag::DebugLuauFreezeDuringUnification)
freeze(currentModule->internalTypes);
@ -928,14 +933,14 @@ ErrorVec TypeChecker::tryUnify_(Id subTy, Id superTy, const Location& location)
return state.errors;
}
ErrorVec TypeChecker::tryUnify(TypeId subTy, TypeId superTy, const Location& location)
ErrorVec TypeChecker::tryUnify(TypeId subTy, TypeId superTy, const ScopePtr& scope, const Location& location)
{
return tryUnify_(subTy, superTy, location);
return tryUnify_(subTy, superTy, scope, location);
}
ErrorVec TypeChecker::tryUnify(TypePackId subTy, TypePackId superTy, const Location& location)
ErrorVec TypeChecker::tryUnify(TypePackId subTy, TypePackId superTy, const ScopePtr& scope, const Location& location)
{
return tryUnify_(subTy, superTy, location);
return tryUnify_(subTy, superTy, scope, location);
}
void TypeChecker::check(const ScopePtr& scope, const AstStatAssign& assign)
@ -1029,9 +1034,9 @@ void TypeChecker::check(const ScopePtr& scope, const AstStatAssign& assign)
{
// In nonstrict mode, any assignments where the lhs is free and rhs isn't a function, we give it any typevar.
if (isNonstrictMode() && get<FreeTypeVar>(follow(left)) && !get<FunctionTypeVar>(follow(right)))
unify(anyType, left, loc);
unify(anyType, left, scope, loc);
else
unify(right, left, loc);
unify(right, left, scope, loc);
}
}
}
@ -1046,7 +1051,7 @@ void TypeChecker::check(const ScopePtr& scope, const AstStatCompoundAssign& assi
TypeId result = checkBinaryOperation(scope, expr, left, right);
unify(result, left, assign.location);
unify(result, left, scope, assign.location);
}
void TypeChecker::check(const ScopePtr& scope, const AstStatLocal& local)
@ -1101,7 +1106,7 @@ void TypeChecker::check(const ScopePtr& scope, const AstStatLocal& local)
TypePackId valuePack =
checkExprList(scope, local.location, local.values, /* substituteFreeForNil= */ true, instantiateGenerics, expectedTypes).type;
Unifier state = mkUnifier(local.location);
Unifier state = mkUnifier(scope, local.location);
state.ctx = CountMismatch::Result;
state.tryUnify(valuePack, variablePack);
reportErrors(state.errors);
@ -1177,7 +1182,7 @@ void TypeChecker::check(const ScopePtr& scope, const AstStatFor& expr)
TypeId loopVarType = numberType;
if (expr.var->annotation)
unify(loopVarType, resolveType(scope, *expr.var->annotation), expr.location);
unify(loopVarType, resolveType(scope, *expr.var->annotation), scope, expr.location);
loopScope->bindings[expr.var] = {loopVarType, expr.var->location};
@ -1187,11 +1192,11 @@ void TypeChecker::check(const ScopePtr& scope, const AstStatFor& expr)
if (!expr.to)
ice("Bad AstStatFor has no to expr");
unify(checkExpr(loopScope, *expr.from).type, loopVarType, expr.from->location);
unify(checkExpr(loopScope, *expr.to).type, loopVarType, expr.to->location);
unify(checkExpr(loopScope, *expr.from).type, loopVarType, scope, expr.from->location);
unify(checkExpr(loopScope, *expr.to).type, loopVarType, scope, expr.to->location);
if (expr.step)
unify(checkExpr(loopScope, *expr.step).type, loopVarType, expr.step->location);
unify(checkExpr(loopScope, *expr.step).type, loopVarType, scope, expr.step->location);
check(loopScope, *expr.body);
}
@ -1244,12 +1249,12 @@ void TypeChecker::check(const ScopePtr& scope, const AstStatForIn& forin)
if (get<Unifiable::Free>(callRetPack))
{
iterTy = freshType(scope);
unify(callRetPack, addTypePack({{iterTy}, freshTypePack(scope)}), forin.location);
unify(callRetPack, addTypePack({{iterTy}, freshTypePack(scope)}), scope, forin.location);
}
else if (get<Unifiable::Error>(callRetPack) || !first(callRetPack))
{
for (TypeId var : varTypes)
unify(errorRecoveryType(scope), var, forin.location);
unify(errorRecoveryType(scope), var, scope, forin.location);
return check(loopScope, *forin.body);
}
@ -1270,7 +1275,7 @@ void TypeChecker::check(const ScopePtr& scope, const AstStatForIn& forin)
// TODO: this needs to typecheck all returned values by __iter as if they were for loop arguments
// the structure of the function makes it difficult to do this especially since we don't have actual expressions, only types
for (TypeId var : varTypes)
unify(anyType, var, forin.location);
unify(anyType, var, scope, forin.location);
return check(loopScope, *forin.body);
}
@ -1282,25 +1287,25 @@ void TypeChecker::check(const ScopePtr& scope, const AstStatForIn& forin)
if (iterTable->indexer)
{
if (varTypes.size() > 0)
unify(iterTable->indexer->indexType, varTypes[0], forin.location);
unify(iterTable->indexer->indexType, varTypes[0], scope, forin.location);
if (varTypes.size() > 1)
unify(iterTable->indexer->indexResultType, varTypes[1], forin.location);
unify(iterTable->indexer->indexResultType, varTypes[1], scope, forin.location);
for (size_t i = 2; i < varTypes.size(); ++i)
unify(nilType, varTypes[i], forin.location);
unify(nilType, varTypes[i], scope, forin.location);
}
else if (isNonstrictMode())
{
for (TypeId var : varTypes)
unify(anyType, var, forin.location);
unify(anyType, var, scope, forin.location);
}
else
{
TypeId varTy = errorRecoveryType(loopScope);
for (TypeId var : varTypes)
unify(varTy, var, forin.location);
unify(varTy, var, scope, forin.location);
reportError(firstValue->location, GenericError{"Cannot iterate over a table without indexer"});
}
@ -1314,7 +1319,7 @@ void TypeChecker::check(const ScopePtr& scope, const AstStatForIn& forin)
TypeId varTy = get<AnyTypeVar>(iterTy) ? anyType : errorRecoveryType(loopScope);
for (TypeId var : varTypes)
unify(varTy, var, forin.location);
unify(varTy, var, scope, forin.location);
if (!get<ErrorTypeVar>(iterTy) && !get<AnyTypeVar>(iterTy) && !get<FreeTypeVar>(iterTy) && !get<NeverTypeVar>(iterTy))
reportError(firstValue->location, CannotCallNonFunction{iterTy});
@ -1339,7 +1344,7 @@ void TypeChecker::check(const ScopePtr& scope, const AstStatForIn& forin)
argPack = addTypePack(TypePack{});
}
Unifier state = mkUnifier(firstValue->location);
Unifier state = mkUnifier(loopScope, firstValue->location);
checkArgumentList(loopScope, state, argPack, iterFunc->argTypes, /*argLocations*/ {});
state.log.commit();
@ -1358,10 +1363,10 @@ void TypeChecker::check(const ScopePtr& scope, const AstStatForIn& forin)
AstExprCall exprCall{Location(start, end), firstValue, arguments, /* self= */ false, Location()};
TypePackId retPack = checkExprPack(scope, exprCall).type;
unify(retPack, varPack, forin.location);
unify(retPack, varPack, scope, forin.location);
}
else
unify(iterFunc->retTypes, varPack, forin.location);
unify(iterFunc->retTypes, varPack, scope, forin.location);
check(loopScope, *forin.body);
}
@ -1596,7 +1601,7 @@ void TypeChecker::check(const ScopePtr& scope, const AstStatTypeAlias& typealias
TypeId& bindingType = bindingsMap[name].type;
if (unify(ty, bindingType, typealias.location))
if (unify(ty, bindingType, aliasScope, typealias.location))
bindingType = ty;
if (FFlag::LuauLowerBoundsCalculation)
@ -1886,7 +1891,7 @@ WithPredicate<TypeId> TypeChecker::checkExpr(const ScopePtr& scope, const AstExp
TypeLevel level = FFlag::LuauLowerBoundsCalculation ? ftp->level : scope->level;
TypeId head = freshType(level);
TypePackId pack = addTypePack(TypePackVar{TypePack{{head}, freshTypePack(level)}});
unify(pack, retPack, expr.location);
unify(pack, retPack, scope, expr.location);
return {head, std::move(result.predicates)};
}
if (get<Unifiable::Error>(retPack))
@ -1927,7 +1932,7 @@ std::optional<TypeId> TypeChecker::findTablePropertyRespectingMeta(TypeId lhsTyp
{
ErrorVec errors;
auto result = Luau::findTablePropertyRespectingMeta(errors, lhsType, name, location);
if (!FFlag::LuauIndexSilenceErrors || addErrors)
if (addErrors)
reportErrors(errors);
return result;
}
@ -1936,7 +1941,7 @@ std::optional<TypeId> TypeChecker::findMetatableEntry(TypeId type, std::string e
{
ErrorVec errors;
auto result = Luau::findMetatableEntry(errors, type, entry, location);
if (!FFlag::LuauIndexSilenceErrors || addErrors)
if (addErrors)
reportErrors(errors);
return result;
}
@ -1948,7 +1953,7 @@ std::optional<TypeId> TypeChecker::getIndexTypeFromType(
std::optional<TypeId> result = getIndexTypeFromTypeImpl(scope, type, name, location, addErrors);
if (FFlag::LuauIndexSilenceErrors && !addErrors)
if (!addErrors)
LUAU_ASSERT(errorCount == currentModule->errors.size());
return result;
@ -1978,20 +1983,15 @@ std::optional<TypeId> TypeChecker::getIndexTypeFromTypeImpl(
else if (auto indexer = tableType->indexer)
{
// TODO: Property lookup should work with string singletons or unions thereof as the indexer key type.
ErrorVec errors = tryUnify(stringType, indexer->indexType, location);
ErrorVec errors = tryUnify(stringType, indexer->indexType, scope, location);
if (FFlag::LuauReportErrorsOnIndexerKeyMismatch)
{
if (errors.empty())
return indexer->indexResultType;
if (addErrors)
reportError(location, UnknownProperty{type, name});
return std::nullopt;
}
else
if (errors.empty())
return indexer->indexResultType;
if (addErrors)
reportError(location, UnknownProperty{type, name});
return std::nullopt;
}
else if (tableType->state == TableState::Free)
{
@ -2223,8 +2223,8 @@ TypeId TypeChecker::checkExprTable(
if (indexer)
{
unify(numberType, indexer->indexType, value->location);
unify(valueType, indexer->indexResultType, value->location);
unify(numberType, indexer->indexType, scope, value->location);
unify(valueType, indexer->indexResultType, scope, value->location);
}
else
indexer = TableIndexer{numberType, anyIfNonstrict(valueType)};
@ -2243,13 +2243,13 @@ TypeId TypeChecker::checkExprTable(
if (it != expectedTable->props.end())
{
Property expectedProp = it->second;
ErrorVec errors = tryUnify(exprType, expectedProp.type, k->location);
ErrorVec errors = tryUnify(exprType, expectedProp.type, scope, k->location);
if (errors.empty())
exprType = expectedProp.type;
}
else if (expectedTable->indexer && maybeString(expectedTable->indexer->indexType))
{
ErrorVec errors = tryUnify(exprType, expectedTable->indexer->indexResultType, k->location);
ErrorVec errors = tryUnify(exprType, expectedTable->indexer->indexResultType, scope, k->location);
if (errors.empty())
exprType = expectedTable->indexer->indexResultType;
}
@ -2264,8 +2264,8 @@ TypeId TypeChecker::checkExprTable(
if (indexer)
{
unify(keyType, indexer->indexType, k->location);
unify(valueType, indexer->indexResultType, value->location);
unify(keyType, indexer->indexType, scope, k->location);
unify(valueType, indexer->indexResultType, scope, value->location);
}
else if (isNonstrictMode())
{
@ -2406,7 +2406,7 @@ WithPredicate<TypeId> TypeChecker::checkExpr(const ScopePtr& scope, const AstExp
TypePackId retTypePack = freshTypePack(scope);
TypeId expectedFunctionType = addType(FunctionTypeVar(scope->level, arguments, retTypePack));
Unifier state = mkUnifier(expr.location);
Unifier state = mkUnifier(scope, expr.location);
state.tryUnify(actualFunctionType, expectedFunctionType, /*isFunctionCall*/ true);
state.log.commit();
@ -2424,7 +2424,7 @@ WithPredicate<TypeId> TypeChecker::checkExpr(const ScopePtr& scope, const AstExp
return {errorRecoveryType(scope)};
}
reportErrors(tryUnify(operandType, numberType, expr.location));
reportErrors(tryUnify(operandType, numberType, scope, expr.location));
return {numberType};
}
case AstExprUnary::Len:
@ -2454,7 +2454,7 @@ WithPredicate<TypeId> TypeChecker::checkExpr(const ScopePtr& scope, const AstExp
TypePackId retTypePack = addTypePack({numberType});
TypeId expectedFunctionType = addType(FunctionTypeVar(scope->level, arguments, retTypePack));
Unifier state = mkUnifier(expr.location);
Unifier state = mkUnifier(scope, expr.location);
state.tryUnify(actualFunctionType, expectedFunctionType, /*isFunctionCall*/ true);
state.log.commit();
@ -2504,11 +2504,11 @@ std::string opToMetaTableEntry(const AstExprBinary::Op& op)
}
}
TypeId TypeChecker::unionOfTypes(TypeId a, TypeId b, const Location& location, bool unifyFreeTypes)
TypeId TypeChecker::unionOfTypes(TypeId a, TypeId b, const ScopePtr& scope, const Location& location, bool unifyFreeTypes)
{
if (unifyFreeTypes && (get<FreeTypeVar>(a) || get<FreeTypeVar>(b)))
{
if (unify(b, a, location))
if (unify(b, a, scope, location))
return a;
return errorRecoveryType(anyType);
@ -2583,7 +2583,7 @@ TypeId TypeChecker::checkRelationalOperation(
{
ScopePtr subScope = childScope(scope, subexp->location);
resolve(predicates, subScope, true);
return unionOfTypes(rhsType, stripNil(checkExpr(subScope, *subexp->right).type, true), expr.location);
return unionOfTypes(rhsType, stripNil(checkExpr(subScope, *subexp->right).type, true), subScope, expr.location);
}
}
@ -2619,7 +2619,7 @@ TypeId TypeChecker::checkRelationalOperation(
* report any problems that might have been surfaced as a result of this step because we might already
* have a better, more descriptive error teed up.
*/
Unifier state = mkUnifier(expr.location);
Unifier state = mkUnifier(scope, expr.location);
if (!isEquality)
{
state.tryUnify(rhsType, lhsType);
@ -2698,7 +2698,7 @@ TypeId TypeChecker::checkRelationalOperation(
{
if (isEquality)
{
Unifier state = mkUnifier(expr.location);
Unifier state = mkUnifier(scope, expr.location);
state.tryUnify(addTypePack({booleanType}), ftv->retTypes);
if (!state.errors.empty())
@ -2750,11 +2750,11 @@ TypeId TypeChecker::checkRelationalOperation(
case AstExprBinary::And:
if (lhsIsAny)
return lhsType;
return unionOfTypes(rhsType, booleanType, expr.location, false);
return unionOfTypes(rhsType, booleanType, scope, expr.location, false);
case AstExprBinary::Or:
if (lhsIsAny)
return lhsType;
return unionOfTypes(lhsType, rhsType, expr.location);
return unionOfTypes(lhsType, rhsType, scope, expr.location);
default:
LUAU_ASSERT(0);
ice(format("checkRelationalOperation called with incorrect binary expression '%s'", toString(expr.op).c_str()), expr.location);
@ -2811,7 +2811,7 @@ TypeId TypeChecker::checkBinaryOperation(
}
if (get<FreeTypeVar>(rhsType))
unify(rhsType, lhsType, expr.location);
unify(rhsType, lhsType, scope, expr.location);
if (typeCouldHaveMetatable(lhsType) || typeCouldHaveMetatable(rhsType))
{
@ -2821,7 +2821,7 @@ TypeId TypeChecker::checkBinaryOperation(
TypePackId retTypePack = freshTypePack(scope);
TypeId expectedFunctionType = addType(FunctionTypeVar(scope->level, arguments, retTypePack));
Unifier state = mkUnifier(expr.location);
Unifier state = mkUnifier(scope, expr.location);
state.tryUnify(actualFunctionType, expectedFunctionType, /*isFunctionCall*/ true);
reportErrors(state.errors);
@ -2871,8 +2871,8 @@ TypeId TypeChecker::checkBinaryOperation(
switch (expr.op)
{
case AstExprBinary::Concat:
reportErrors(tryUnify(lhsType, addType(UnionTypeVar{{stringType, numberType}}), expr.left->location));
reportErrors(tryUnify(rhsType, addType(UnionTypeVar{{stringType, numberType}}), expr.right->location));
reportErrors(tryUnify(lhsType, addType(UnionTypeVar{{stringType, numberType}}), scope, expr.left->location));
reportErrors(tryUnify(rhsType, addType(UnionTypeVar{{stringType, numberType}}), scope, expr.right->location));
return stringType;
case AstExprBinary::Add:
case AstExprBinary::Sub:
@ -2880,8 +2880,8 @@ TypeId TypeChecker::checkBinaryOperation(
case AstExprBinary::Div:
case AstExprBinary::Mod:
case AstExprBinary::Pow:
reportErrors(tryUnify(lhsType, numberType, expr.left->location));
reportErrors(tryUnify(rhsType, numberType, expr.right->location));
reportErrors(tryUnify(lhsType, numberType, scope, expr.left->location));
reportErrors(tryUnify(rhsType, numberType, scope, expr.right->location));
return numberType;
default:
// These should have been handled with checkRelationalOperation
@ -2956,10 +2956,10 @@ WithPredicate<TypeId> TypeChecker::checkExpr(const ScopePtr& scope, const AstExp
WithPredicate<TypeId> result = checkExpr(scope, *expr.expr, annotationType);
// Note: As an optimization, we try 'number <: number | string' first, as that is the more likely case.
if (canUnify(annotationType, result.type, expr.location).empty())
if (canUnify(annotationType, result.type, scope, expr.location).empty())
return {annotationType, std::move(result.predicates)};
if (canUnify(result.type, annotationType, expr.location).empty())
if (canUnify(result.type, annotationType, scope, expr.location).empty())
return {annotationType, std::move(result.predicates)};
reportError(expr.location, TypesAreUnrelated{result.type, annotationType});
@ -3104,7 +3104,7 @@ TypeId TypeChecker::checkLValueBinding(const ScopePtr& scope, const AstExprIndex
}
else if (auto indexer = lhsTable->indexer)
{
Unifier state = mkUnifier(expr.location);
Unifier state = mkUnifier(scope, expr.location);
state.tryUnify(stringType, indexer->indexType);
TypeId retType = indexer->indexResultType;
if (!state.errors.empty())
@ -3216,7 +3216,7 @@ TypeId TypeChecker::checkLValueBinding(const ScopePtr& scope, const AstExprIndex
if (exprTable->indexer)
{
const TableIndexer& indexer = *exprTable->indexer;
unify(indexType, indexer.indexType, expr.index->location);
unify(indexType, indexer.indexType, scope, expr.index->location);
return indexer.indexResultType;
}
else if (exprTable->state == TableState::Unsealed || exprTable->state == TableState::Free)
@ -3832,7 +3832,7 @@ void TypeChecker::checkArgumentList(
{
// The use of unify here is deliberate. We don't want this unification
// to be undoable.
unify(errorRecoveryType(scope), *argIter, state.location);
unify(errorRecoveryType(scope), *argIter, scope, state.location);
++argIter;
}
reportCountMismatchError();
@ -3855,7 +3855,7 @@ void TypeChecker::checkArgumentList(
TypeId e = errorRecoveryType(scope);
while (argIter != endIter)
{
unify(e, *argIter, state.location);
unify(e, *argIter, scope, state.location);
++argIter;
}
@ -3872,7 +3872,7 @@ void TypeChecker::checkArgumentList(
if (argIndex < argLocations.size())
location = argLocations[argIndex];
unify(*argIter, vtp->ty, location);
unify(*argIter, vtp->ty, scope, location);
++argIter;
++argIndex;
}
@ -3909,7 +3909,7 @@ void TypeChecker::checkArgumentList(
}
else
{
unifyWithInstantiationIfNeeded(scope, *argIter, *paramIter, state);
unifyWithInstantiationIfNeeded(*argIter, *paramIter, scope, state);
++argIter;
++paramIter;
}
@ -4117,7 +4117,7 @@ std::optional<WithPredicate<TypePackId>> TypeChecker::checkCallOverload(const Sc
if (get<AnyTypeVar>(fn))
{
unify(anyTypePack, argPack, expr.location);
unify(anyTypePack, argPack, scope, expr.location);
return {{anyTypePack}};
}
@ -4163,7 +4163,7 @@ std::optional<WithPredicate<TypePackId>> TypeChecker::checkCallOverload(const Sc
UnifierOptions options;
options.isFunctionCall = true;
unify(r, fn, expr.location, options);
unify(r, fn, scope, expr.location, options);
return {{retPack}};
}
@ -4197,7 +4197,7 @@ std::optional<WithPredicate<TypePackId>> TypeChecker::checkCallOverload(const Sc
if (!ftv)
{
reportError(TypeError{expr.func->location, CannotCallNonFunction{fn}});
unify(errorRecoveryTypePack(scope), retPack, expr.func->location);
unify(errorRecoveryTypePack(scope), retPack, scope, expr.func->location);
return {{errorRecoveryTypePack(retPack)}};
}
@ -4210,7 +4210,7 @@ std::optional<WithPredicate<TypePackId>> TypeChecker::checkCallOverload(const Sc
return *ret;
}
Unifier state = mkUnifier(expr.location);
Unifier state = mkUnifier(scope, expr.location);
// Unify return types
checkArgumentList(scope, state, retPack, ftv->retTypes, /*argLocations*/ {});
@ -4272,7 +4272,7 @@ bool TypeChecker::handleSelfCallMismatch(const ScopePtr& scope, const AstExprCal
std::vector<TypeId> editedParamList(args->head.begin() + 1, args->head.end());
TypePackId editedArgPack = addTypePack(TypePack{editedParamList});
Unifier editedState = mkUnifier(expr.location);
Unifier editedState = mkUnifier(scope, expr.location);
checkArgumentList(scope, editedState, editedArgPack, ftv->argTypes, editedArgLocations);
if (editedState.errors.empty())
@ -4302,7 +4302,7 @@ bool TypeChecker::handleSelfCallMismatch(const ScopePtr& scope, const AstExprCal
editedArgList.insert(editedArgList.begin(), checkExpr(scope, *indexName->expr).type);
TypePackId editedArgPack = addTypePack(TypePack{editedArgList});
Unifier editedState = mkUnifier(expr.location);
Unifier editedState = mkUnifier(scope, expr.location);
checkArgumentList(scope, editedState, editedArgPack, ftv->argTypes, editedArgLocations);
@ -4368,7 +4368,7 @@ void TypeChecker::reportOverloadResolutionError(const ScopePtr& scope, const Ast
for (size_t i = 0; i < overloadTypes.size(); ++i)
{
TypeId overload = overloadTypes[i];
Unifier state = mkUnifier(expr.location);
Unifier state = mkUnifier(scope, expr.location);
// Unify return types
if (const FunctionTypeVar* ftv = get<FunctionTypeVar>(overload))
@ -4418,7 +4418,7 @@ WithPredicate<TypePackId> TypeChecker::checkExprList(const ScopePtr& scope, cons
size_t lastIndex = exprs.size - 1;
tp->head.reserve(lastIndex);
Unifier state = mkUnifier(location);
Unifier state = mkUnifier(scope, location);
std::vector<TxnLog> inverseLogs;
@ -4583,15 +4583,15 @@ TypeId TypeChecker::anyIfNonstrict(TypeId ty) const
return ty;
}
bool TypeChecker::unify(TypeId subTy, TypeId superTy, const Location& location)
bool TypeChecker::unify(TypeId subTy, TypeId superTy, const ScopePtr& scope, const Location& location)
{
UnifierOptions options;
return unify(subTy, superTy, location, options);
return unify(subTy, superTy, scope, location, options);
}
bool TypeChecker::unify(TypeId subTy, TypeId superTy, const Location& location, const UnifierOptions& options)
bool TypeChecker::unify(TypeId subTy, TypeId superTy, const ScopePtr& scope, const Location& location, const UnifierOptions& options)
{
Unifier state = mkUnifier(location);
Unifier state = mkUnifier(scope, location);
state.tryUnify(subTy, superTy, options.isFunctionCall);
state.log.commit();
@ -4601,9 +4601,9 @@ bool TypeChecker::unify(TypeId subTy, TypeId superTy, const Location& location,
return state.errors.empty();
}
bool TypeChecker::unify(TypePackId subTy, TypePackId superTy, const Location& location, CountMismatch::Context ctx)
bool TypeChecker::unify(TypePackId subTy, TypePackId superTy, const ScopePtr& scope, const Location& location, CountMismatch::Context ctx)
{
Unifier state = mkUnifier(location);
Unifier state = mkUnifier(scope, location);
state.ctx = ctx;
state.tryUnify(subTy, superTy);
@ -4614,10 +4614,10 @@ bool TypeChecker::unify(TypePackId subTy, TypePackId superTy, const Location& lo
return state.errors.empty();
}
bool TypeChecker::unifyWithInstantiationIfNeeded(const ScopePtr& scope, TypeId subTy, TypeId superTy, const Location& location)
bool TypeChecker::unifyWithInstantiationIfNeeded(TypeId subTy, TypeId superTy, const ScopePtr& scope, const Location& location)
{
Unifier state = mkUnifier(location);
unifyWithInstantiationIfNeeded(scope, subTy, superTy, state);
Unifier state = mkUnifier(scope, location);
unifyWithInstantiationIfNeeded(subTy, superTy, scope, state);
state.log.commit();
@ -4626,7 +4626,7 @@ bool TypeChecker::unifyWithInstantiationIfNeeded(const ScopePtr& scope, TypeId s
return state.errors.empty();
}
void TypeChecker::unifyWithInstantiationIfNeeded(const ScopePtr& scope, TypeId subTy, TypeId superTy, Unifier& state)
void TypeChecker::unifyWithInstantiationIfNeeded(TypeId subTy, TypeId superTy, const ScopePtr& scope, Unifier& state)
{
if (!maybeGeneric(subTy))
// Quick check to see if we definitely can't instantiate
@ -4665,77 +4665,6 @@ void TypeChecker::unifyWithInstantiationIfNeeded(const ScopePtr& scope, TypeId s
}
}
bool Anyification::isDirty(TypeId ty)
{
if (ty->persistent)
return false;
if (const TableTypeVar* ttv = log->getMutable<TableTypeVar>(ty))
return (ttv->state == TableState::Free || ttv->state == TableState::Unsealed);
else if (log->getMutable<FreeTypeVar>(ty))
return true;
else if (get<ConstrainedTypeVar>(ty))
return true;
else
return false;
}
bool Anyification::isDirty(TypePackId tp)
{
if (tp->persistent)
return false;
if (log->getMutable<FreeTypePack>(tp))
return true;
else
return false;
}
TypeId Anyification::clean(TypeId ty)
{
LUAU_ASSERT(isDirty(ty));
if (const TableTypeVar* ttv = log->getMutable<TableTypeVar>(ty))
{
TableTypeVar clone = TableTypeVar{ttv->props, ttv->indexer, ttv->level, TableState::Sealed};
clone.definitionModuleName = ttv->definitionModuleName;
clone.name = ttv->name;
clone.syntheticName = ttv->syntheticName;
clone.tags = ttv->tags;
TypeId res = addType(std::move(clone));
asMutable(res)->normal = ty->normal;
return res;
}
else if (auto ctv = get<ConstrainedTypeVar>(ty))
{
if (FFlag::LuauQuantifyConstrained)
{
std::vector<TypeId> copy = ctv->parts;
for (TypeId& ty : copy)
ty = replace(ty);
TypeId res = copy.size() == 1 ? copy[0] : addType(UnionTypeVar{std::move(copy)});
auto [t, ok] = normalize(res, *arena, *iceHandler);
if (!ok)
normalizationTooComplex = true;
return t;
}
else
{
auto [t, ok] = normalize(ty, *arena, *iceHandler);
if (!ok)
normalizationTooComplex = true;
return t;
}
}
else
return anyType;
}
TypePackId Anyification::clean(TypePackId tp)
{
LUAU_ASSERT(isDirty(tp));
return anyTypePack;
}
TypeId TypeChecker::quantify(const ScopePtr& scope, TypeId ty, Location location)
{
ty = follow(ty);
@ -4807,7 +4736,7 @@ TypeId TypeChecker::anyify(const ScopePtr& scope, TypeId ty, Location location)
ty = t;
}
Anyification anyification{&currentModule->internalTypes, iceHandler, anyType, anyTypePack};
Anyification anyification{&currentModule->internalTypes, scope, iceHandler, anyType, anyTypePack};
std::optional<TypeId> any = anyification.substitute(ty);
if (anyification.normalizationTooComplex)
reportError(location, NormalizationTooComplex{});
@ -4830,7 +4759,7 @@ TypePackId TypeChecker::anyify(const ScopePtr& scope, TypePackId ty, Location lo
ty = t;
}
Anyification anyification{&currentModule->internalTypes, iceHandler, anyType, anyTypePack};
Anyification anyification{&currentModule->internalTypes, scope, iceHandler, anyType, anyTypePack};
std::optional<TypePackId> any = anyification.substitute(ty);
if (any.has_value())
return *any;
@ -4966,9 +4895,9 @@ void TypeChecker::merge(RefinementMap& l, const RefinementMap& r)
});
}
Unifier TypeChecker::mkUnifier(const Location& location)
Unifier TypeChecker::mkUnifier(const ScopePtr& scope, const Location& location)
{
return Unifier{&currentModule->internalTypes, currentModule->mode, location, Variance::Covariant, unifierState};
return Unifier{&currentModule->internalTypes, currentModule->mode, NotNull{scope.get()}, location, Variance::Covariant, unifierState};
}
TypeId TypeChecker::freshType(const ScopePtr& scope)
@ -5032,10 +4961,7 @@ TypeIdPredicate TypeChecker::mkTruthyPredicate(bool sense)
return sense ? std::nullopt : std::optional<TypeId>(ty);
// at this point, anything else is kept if sense is true, or replaced by nil
if (FFlag::LuauFalsyPredicateReturnsNilInstead)
return sense ? ty : nilType;
else
return sense ? std::optional<TypeId>(ty) : std::nullopt;
return sense ? ty : nilType;
};
}
@ -5878,8 +5804,8 @@ void TypeChecker::resolve(const IsAPredicate& isaP, RefinementMap& refis, const
{
auto predicate = [&](TypeId option) -> std::optional<TypeId> {
// This by itself is not truly enough to determine that A is stronger than B or vice versa.
bool optionIsSubtype = canUnify(option, isaP.ty, isaP.location).empty();
bool targetIsSubtype = canUnify(isaP.ty, option, isaP.location).empty();
bool optionIsSubtype = canUnify(option, isaP.ty, scope, isaP.location).empty();
bool targetIsSubtype = canUnify(isaP.ty, option, scope, isaP.location).empty();
// If A is a superset of B, then if sense is true, we promote A to B, otherwise we keep A.
if (!optionIsSubtype && targetIsSubtype)
@ -6022,7 +5948,7 @@ void TypeChecker::resolve(const EqPredicate& eqP, RefinementMap& refis, const Sc
if (maybeSingleton(eqP.type))
{
// Normally we'd write option <: eqP.type, but singletons are always the subtype, so we flip this.
if (!sense || canUnify(eqP.type, option, eqP.location).empty())
if (!sense || canUnify(eqP.type, option, scope, eqP.location).empty())
return sense ? eqP.type : option;
// local variable works around an odd gcc 9.3 warning: <anonymous> may be used uninitialized
@ -6056,7 +5982,7 @@ std::vector<TypeId> TypeChecker::unTypePack(const ScopePtr& scope, TypePackId tp
size_t oldErrorsSize = currentModule->errors.size();
unify(tp, expectedTypePack, location);
unify(tp, expectedTypePack, scope, location);
// HACK: tryUnify would undo the changes to the expectedTypePack if the length mismatches, but
// we want to tie up free types to be error types, so we do this instead.

View file

@ -1,6 +1,7 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#include "Luau/TypeUtils.h"
#include "Luau/Normalize.h"
#include "Luau/Scope.h"
#include "Luau/ToString.h"
#include "Luau/TypeInfer.h"
@ -8,7 +9,7 @@
namespace Luau
{
std::optional<TypeId> findMetatableEntry(ErrorVec& errors, TypeId type, std::string entry, Location location)
std::optional<TypeId> findMetatableEntry(ErrorVec& errors, TypeId type, const std::string& entry, Location location)
{
type = follow(type);
@ -35,7 +36,7 @@ std::optional<TypeId> findMetatableEntry(ErrorVec& errors, TypeId type, std::str
return std::nullopt;
}
std::optional<TypeId> findTablePropertyRespectingMeta(ErrorVec& errors, TypeId ty, Name name, Location location)
std::optional<TypeId> findTablePropertyRespectingMeta(ErrorVec& errors, TypeId ty, const std::string& name, Location location)
{
if (get<AnyTypeVar>(ty))
return ty;
@ -83,4 +84,110 @@ std::optional<TypeId> findTablePropertyRespectingMeta(ErrorVec& errors, TypeId t
return std::nullopt;
}
std::optional<TypeId> getIndexTypeFromType(
const ScopePtr& scope, ErrorVec& errors, TypeArena* arena, TypeId type, const std::string& prop, const Location& location, bool addErrors,
InternalErrorReporter& handle)
{
type = follow(type);
if (get<ErrorTypeVar>(type) || get<AnyTypeVar>(type) || get<NeverTypeVar>(type))
return type;
if (auto f = get<FreeTypeVar>(type))
*asMutable(type) = TableTypeVar{TableState::Free, f->level};
if (isString(type))
{
std::optional<TypeId> mtIndex = Luau::findMetatableEntry(errors, getSingletonTypes().stringType, "__index", location);
LUAU_ASSERT(mtIndex);
type = *mtIndex;
}
if (getTableType(type))
{
return findTablePropertyRespectingMeta(errors, type, prop, location);
}
else if (const ClassTypeVar* cls = get<ClassTypeVar>(type))
{
if (const Property* p = lookupClassProp(cls, prop))
return p->type;
}
else if (const UnionTypeVar* utv = get<UnionTypeVar>(type))
{
std::vector<TypeId> goodOptions;
std::vector<TypeId> badOptions;
for (TypeId t : utv)
{
// TODO: we should probably limit recursion here?
// RecursionLimiter _rl(&recursionCount, FInt::LuauTypeInferRecursionLimit);
// Not needed when we normalize types.
if (get<AnyTypeVar>(follow(t)))
return t;
if (std::optional<TypeId> ty = getIndexTypeFromType(scope, errors, arena, t, prop, location, /* addErrors= */ false, handle))
goodOptions.push_back(*ty);
else
badOptions.push_back(t);
}
if (!badOptions.empty())
{
if (addErrors)
{
if (goodOptions.empty())
errors.push_back(TypeError{location, UnknownProperty{type, prop}});
else
errors.push_back(TypeError{location, MissingUnionProperty{type, badOptions, prop}});
}
return std::nullopt;
}
if (goodOptions.empty())
return getSingletonTypes().neverType;
if (goodOptions.size() == 1)
return goodOptions[0];
// TODO: inefficient.
TypeId result = arena->addType(UnionTypeVar{std::move(goodOptions)});
auto [ty, ok] = normalize(result, NotNull{scope.get()}, *arena, handle);
if (!ok && addErrors)
errors.push_back(TypeError{location, NormalizationTooComplex{}});
return ok ? ty : getSingletonTypes().anyType;
}
else if (const IntersectionTypeVar* itv = get<IntersectionTypeVar>(type))
{
std::vector<TypeId> parts;
for (TypeId t : itv->parts)
{
// TODO: we should probably limit recursion here?
// RecursionLimiter _rl(&recursionCount, FInt::LuauTypeInferRecursionLimit);
if (std::optional<TypeId> ty = getIndexTypeFromType(scope, errors, arena, t, prop, location, /* addErrors= */ false, handle))
parts.push_back(*ty);
}
// If no parts of the intersection had the property we looked up for, it never existed at all.
if (parts.empty())
{
if (addErrors)
errors.push_back(TypeError{location, UnknownProperty{type, prop}});
return std::nullopt;
}
if (parts.size() == 1)
return parts[0];
return arena->addType(IntersectionTypeVar{std::move(parts)}); // Not at all correct.
}
if (addErrors)
errors.push_back(TypeError{location, UnknownProperty{type, prop}});
return std::nullopt;
}
} // namespace Luau

View file

@ -1135,7 +1135,7 @@ std::optional<WithPredicate<TypePackId>> magicFunctionFormat(
{
Location location = expr.args.data[std::min(i + dataOffset, expr.args.size - 1)]->location;
typechecker.unify(params[i + paramOffset], expected[i], location);
typechecker.unify(params[i + paramOffset], expected[i], scope, location);
}
// if we know the argument count or if we have too many arguments for sure, we can issue an error
@ -1234,7 +1234,7 @@ static std::optional<WithPredicate<TypePackId>> magicFunctionGmatch(
if (returnTypes.empty())
return std::nullopt;
typechecker.unify(params[0], typechecker.stringType, expr.args.data[0]->location);
typechecker.unify(params[0], typechecker.stringType, scope, expr.args.data[0]->location);
const TypePackId emptyPack = arena.addTypePack({});
const TypePackId returnList = arena.addTypePack(returnTypes);
@ -1269,13 +1269,13 @@ static std::optional<WithPredicate<TypePackId>> magicFunctionMatch(
if (returnTypes.empty())
return std::nullopt;
typechecker.unify(params[0], typechecker.stringType, expr.args.data[0]->location);
typechecker.unify(params[0], typechecker.stringType, scope, expr.args.data[0]->location);
const TypeId optionalNumber = arena.addType(UnionTypeVar{{typechecker.nilType, typechecker.numberType}});
size_t initIndex = expr.self ? 1 : 2;
if (params.size() == 3 && expr.args.size > initIndex)
typechecker.unify(params[2], optionalNumber, expr.args.data[initIndex]->location);
typechecker.unify(params[2], optionalNumber, scope, expr.args.data[initIndex]->location);
const TypePackId returnList = arena.addTypePack(returnTypes);
return WithPredicate<TypePackId>{returnList};
@ -1320,17 +1320,17 @@ static std::optional<WithPredicate<TypePackId>> magicFunctionFind(
return std::nullopt;
}
typechecker.unify(params[0], typechecker.stringType, expr.args.data[0]->location);
typechecker.unify(params[0], typechecker.stringType, scope, expr.args.data[0]->location);
const TypeId optionalNumber = arena.addType(UnionTypeVar{{typechecker.nilType, typechecker.numberType}});
const TypeId optionalBoolean = arena.addType(UnionTypeVar{{typechecker.nilType, typechecker.booleanType}});
size_t initIndex = expr.self ? 1 : 2;
if (params.size() >= 3 && expr.args.size > initIndex)
typechecker.unify(params[2], optionalNumber, expr.args.data[initIndex]->location);
typechecker.unify(params[2], optionalNumber, scope, expr.args.data[initIndex]->location);
if (params.size() == 4 && expr.args.size > plainIndex)
typechecker.unify(params[3], optionalBoolean, expr.args.data[plainIndex]->location);
typechecker.unify(params[3], optionalBoolean, scope, expr.args.data[plainIndex]->location);
returnTypes.insert(returnTypes.begin(), {optionalNumber, optionalNumber});

View file

@ -27,27 +27,6 @@ LUAU_FASTFLAG(DebugLuauFreezeArena)
namespace Luau
{
static void* systemAllocateAligned(size_t size, size_t align)
{
#ifdef _WIN32
return _aligned_malloc(size, align);
#elif defined(__ANDROID__) // for Android 4.1
return memalign(align, size);
#else
void* ptr;
return posix_memalign(&ptr, align, size) == 0 ? ptr : 0;
#endif
}
static void systemDeallocateAligned(void* ptr)
{
#ifdef _WIN32
_aligned_free(ptr);
#else
free(ptr);
#endif
}
static size_t pageAlign(size_t size)
{
return (size + kPageSize - 1) & ~(kPageSize - 1);
@ -55,18 +34,31 @@ static size_t pageAlign(size_t size)
void* pagedAllocate(size_t size)
{
if (FFlag::DebugLuauFreezeArena)
return systemAllocateAligned(pageAlign(size), kPageSize);
else
// By default we use operator new/delete instead of malloc/free so that they can be overridden externally
if (!FFlag::DebugLuauFreezeArena)
return ::operator new(size, std::nothrow);
// On Windows, VirtualAlloc results in 64K granularity allocations; we allocate in chunks of ~32K so aligned_malloc is a little more efficient
// On Linux, we must use mmap because using regular heap results in mprotect() fragmenting the page table and us bumping into 64K mmap limit.
#ifdef _WIN32
return _aligned_malloc(size, kPageSize);
#else
return mmap(nullptr, pageAlign(size), PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1, 0);
#endif
}
void pagedDeallocate(void* ptr)
void pagedDeallocate(void* ptr, size_t size)
{
if (FFlag::DebugLuauFreezeArena)
systemDeallocateAligned(ptr);
else
::operator delete(ptr);
// By default we use operator new/delete instead of malloc/free so that they can be overridden externally
if (!FFlag::DebugLuauFreezeArena)
return ::operator delete(ptr);
#ifdef _WIN32
_aligned_free(ptr);
#else
int rc = munmap(ptr, size);
LUAU_ASSERT(rc == 0);
#endif
}
void pagedFreeze(void* ptr, size_t size)

View file

@ -20,8 +20,8 @@ LUAU_FASTINTVARIABLE(LuauTypeInferLowerBoundsIterationLimit, 2000);
LUAU_FASTFLAG(LuauLowerBoundsCalculation);
LUAU_FASTFLAG(LuauErrorRecoveryType);
LUAU_FASTFLAG(LuauUnknownAndNeverType)
LUAU_FASTFLAG(LuauQuantifyConstrained)
LUAU_FASTFLAGVARIABLE(LuauScalarShapeSubtyping, false)
LUAU_FASTFLAG(LuauClassTypeVarsInSubstitution)
namespace Luau
{
@ -273,6 +273,9 @@ TypePackId Widen::clean(TypePackId)
bool Widen::ignoreChildren(TypeId ty)
{
if (FFlag::LuauClassTypeVarsInSubstitution && get<ClassTypeVar>(ty))
return true;
return !log->is<UnionTypeVar>(ty);
}
@ -314,9 +317,10 @@ static std::optional<std::pair<Luau::Name, const SingletonTypeVar*>> getTableMat
return std::nullopt;
}
Unifier::Unifier(TypeArena* types, Mode mode, const Location& location, Variance variance, UnifierSharedState& sharedState, TxnLog* parentLog)
Unifier::Unifier(TypeArena* types, Mode mode, NotNull<Scope> scope, const Location& location, Variance variance, UnifierSharedState& sharedState, TxnLog* parentLog)
: types(types)
, mode(mode)
, scope(scope)
, log(parentLog)
, location(location)
, variance(variance)
@ -370,25 +374,14 @@ void Unifier::tryUnify_(TypeId subTy, TypeId superTy, bool isFunctionCall, bool
if (superFree && subFree && superFree->level.subsumes(subFree->level))
{
occursCheck(subTy, superTy);
// The occurrence check might have caused superTy no longer to be a free type
bool occursFailed = bool(log.getMutable<ErrorTypeVar>(subTy));
if (!occursFailed)
{
if (!occursCheck(subTy, superTy))
log.replace(subTy, BoundTypeVar(superTy));
}
return;
}
else if (superFree && subFree)
{
occursCheck(superTy, subTy);
bool occursFailed = bool(log.getMutable<ErrorTypeVar>(superTy));
if (!occursFailed)
if (!occursCheck(superTy, subTy))
{
if (superFree->level.subsumes(subFree->level))
{
@ -402,24 +395,18 @@ void Unifier::tryUnify_(TypeId subTy, TypeId superTy, bool isFunctionCall, bool
}
else if (superFree)
{
TypeLevel superLevel = superFree->level;
occursCheck(superTy, subTy);
bool occursFailed = bool(log.getMutable<ErrorTypeVar>(superTy));
// Unification can't change the level of a generic.
auto subGeneric = log.getMutable<GenericTypeVar>(subTy);
if (subGeneric && !subGeneric->level.subsumes(superLevel))
if (subGeneric && !subGeneric->level.subsumes(superFree->level))
{
// TODO: a more informative error message? CLI-39912
reportError(TypeError{location, GenericError{"Generic subtype escaping scope"}});
return;
}
// The occurrence check might have caused superTy no longer to be a free type
if (!occursFailed)
if (!occursCheck(superTy, subTy))
{
promoteTypeLevels(log, types, superLevel, subTy);
promoteTypeLevels(log, types, superFree->level, subTy);
Widen widen{types};
log.replace(superTy, BoundTypeVar(widen(subTy)));
@ -437,11 +424,6 @@ void Unifier::tryUnify_(TypeId subTy, TypeId superTy, bool isFunctionCall, bool
return;
}
TypeLevel subLevel = subFree->level;
occursCheck(subTy, superTy);
bool occursFailed = bool(log.getMutable<ErrorTypeVar>(subTy));
// Unification can't change the level of a generic.
auto superGeneric = log.getMutable<GenericTypeVar>(superTy);
if (superGeneric && !superGeneric->level.subsumes(subFree->level))
@ -451,9 +433,9 @@ void Unifier::tryUnify_(TypeId subTy, TypeId superTy, bool isFunctionCall, bool
return;
}
if (!occursFailed)
if (!occursCheck(subTy, superTy))
{
promoteTypeLevels(log, types, subLevel, superTy);
promoteTypeLevels(log, types, subFree->level, superTy);
log.replace(subTy, BoundTypeVar(superTy));
}
@ -1033,9 +1015,7 @@ void Unifier::tryUnify_(TypePackId subTp, TypePackId superTp, bool isFunctionCal
if (log.getMutable<Unifiable::Free>(superTp))
{
occursCheck(superTp, subTp);
if (!log.getMutable<ErrorTypeVar>(superTp))
if (!occursCheck(superTp, subTp))
{
Widen widen{types};
log.replace(superTp, Unifiable::Bound<TypePackId>(widen(subTp)));
@ -1043,9 +1023,7 @@ void Unifier::tryUnify_(TypePackId subTp, TypePackId superTp, bool isFunctionCal
}
else if (log.getMutable<Unifiable::Free>(subTp))
{
occursCheck(subTp, superTp);
if (!log.getMutable<ErrorTypeVar>(subTp))
if (!occursCheck(subTp, superTp))
{
log.replace(subTp, Unifiable::Bound<TypePackId>(superTp));
}
@ -2106,20 +2084,18 @@ void Unifier::unifyLowerBound(TypePackId subTy, TypePackId superTy, TypeLevel de
{
TypePackId tailPack = follow(*t);
if (log.get<FreeTypePack>(tailPack))
occursCheck(tailPack, subTy);
if (log.get<FreeTypePack>(tailPack) && occursCheck(tailPack, subTy))
return;
FreeTypePack* freeTailPack = log.getMutable<FreeTypePack>(tailPack);
if (!freeTailPack)
return;
TypeLevel level = FFlag::LuauQuantifyConstrained ? demotedLevel : freeTailPack->level;
TypePack* tp = getMutable<TypePack>(log.replace(tailPack, TypePack{}));
for (; subIter != subEndIter; ++subIter)
{
tp->head.push_back(types->addType(ConstrainedTypeVar{level, {follow(*subIter)}}));
tp->head.push_back(types->addType(ConstrainedTypeVar{demotedLevel, {follow(*subIter)}}));
}
tp->tail = subIter.tail();
@ -2180,32 +2156,35 @@ void Unifier::unifyLowerBound(TypePackId subTy, TypePackId superTy, TypeLevel de
}
}
void Unifier::occursCheck(TypeId needle, TypeId haystack)
bool Unifier::occursCheck(TypeId needle, TypeId haystack)
{
sharedState.tempSeenTy.clear();
return occursCheck(sharedState.tempSeenTy, needle, haystack);
}
void Unifier::occursCheck(DenseHashSet<TypeId>& seen, TypeId needle, TypeId haystack)
bool Unifier::occursCheck(DenseHashSet<TypeId>& seen, TypeId needle, TypeId haystack)
{
RecursionLimiter _ra(&sharedState.counters.recursionCount,
FFlag::LuauAutocompleteDynamicLimits ? sharedState.counters.recursionLimit : FInt::LuauTypeInferRecursionLimit);
bool occurrence = false;
auto check = [&](TypeId tv) {
occursCheck(seen, needle, tv);
if (occursCheck(seen, needle, tv))
occurrence = true;
};
needle = log.follow(needle);
haystack = log.follow(haystack);
if (seen.find(haystack))
return;
return false;
seen.insert(haystack);
if (log.getMutable<Unifiable::Error>(needle))
return;
return false;
if (!log.getMutable<Unifiable::Free>(needle))
ice("Expected needle to be free");
@ -2215,11 +2194,11 @@ void Unifier::occursCheck(DenseHashSet<TypeId>& seen, TypeId needle, TypeId hays
reportError(TypeError{location, OccursCheckFailed{}});
log.replace(needle, *getSingletonTypes().errorRecoveryType());
return;
return true;
}
if (log.getMutable<FreeTypeVar>(haystack))
return;
return false;
else if (auto a = log.getMutable<UnionTypeVar>(haystack))
{
for (TypeId ty : a->options)
@ -2235,27 +2214,29 @@ void Unifier::occursCheck(DenseHashSet<TypeId>& seen, TypeId needle, TypeId hays
for (TypeId ty : a->parts)
check(ty);
}
return occurrence;
}
void Unifier::occursCheck(TypePackId needle, TypePackId haystack)
bool Unifier::occursCheck(TypePackId needle, TypePackId haystack)
{
sharedState.tempSeenTp.clear();
return occursCheck(sharedState.tempSeenTp, needle, haystack);
}
void Unifier::occursCheck(DenseHashSet<TypePackId>& seen, TypePackId needle, TypePackId haystack)
bool Unifier::occursCheck(DenseHashSet<TypePackId>& seen, TypePackId needle, TypePackId haystack)
{
needle = log.follow(needle);
haystack = log.follow(haystack);
if (seen.find(haystack))
return;
return false;
seen.insert(haystack);
if (log.getMutable<Unifiable::Error>(needle))
return;
return false;
if (!log.getMutable<Unifiable::Free>(needle))
ice("Expected needle pack to be free");
@ -2270,7 +2251,7 @@ void Unifier::occursCheck(DenseHashSet<TypePackId>& seen, TypePackId needle, Typ
reportError(TypeError{location, OccursCheckFailed{}});
log.replace(needle, *getSingletonTypes().errorRecoveryTypePack());
return;
return true;
}
if (auto a = get<TypePack>(haystack); a && a->tail)
@ -2281,11 +2262,13 @@ void Unifier::occursCheck(DenseHashSet<TypePackId>& seen, TypePackId needle, Typ
break;
}
return false;
}
Unifier Unifier::makeChildUnifier()
{
Unifier u = Unifier{types, mode, location, variance, sharedState, &log};
Unifier u = Unifier{types, mode, scope, location, variance, sharedState, &log};
u.anyIsTop = anyIsTop;
return u;
}

View file

@ -138,7 +138,7 @@ private:
// funcbody ::= `(' [parlist] `)' block end
// parlist ::= namelist [`,' `...'] | `...'
std::pair<AstExprFunction*, AstLocal*> parseFunctionBody(
bool hasself, const Lexeme& matchFunction, const AstName& debugname, std::optional<Name> localName);
bool hasself, const Lexeme& matchFunction, const AstName& debugname, const Name* localName);
// explist ::= {exp `,'} exp
void parseExprList(TempVector<AstExpr*>& result);
@ -217,7 +217,7 @@ private:
AstExpr* parseSimpleExpr();
// args ::= `(' [explist] `)' | tableconstructor | String
AstExpr* parseFunctionArgs(AstExpr* func, bool self, const Location& selfLocation);
AstExpr* parseFunctionArgs(AstExpr* func, bool self);
// tableconstructor ::= `{' [fieldlist] `}'
// fieldlist ::= field {fieldsep field} [fieldsep]
@ -244,6 +244,7 @@ private:
std::optional<AstArray<char>> parseCharArray();
AstExpr* parseString();
AstExpr* parseNumber();
AstLocal* pushLocal(const Binding& binding);
@ -256,11 +257,24 @@ private:
bool expectAndConsume(Lexeme::Type type, const char* context = nullptr);
void expectAndConsumeFail(Lexeme::Type type, const char* context);
bool expectMatchAndConsume(char value, const Lexeme& begin, bool searchForMissing = false);
void expectMatchAndConsumeFail(Lexeme::Type type, const Lexeme& begin, const char* extra = nullptr);
struct MatchLexeme
{
MatchLexeme(const Lexeme& l)
: type(l.type)
, position(l.location.begin)
{
}
bool expectMatchEndAndConsume(Lexeme::Type type, const Lexeme& begin);
void expectMatchEndAndConsumeFail(Lexeme::Type type, const Lexeme& begin);
Lexeme::Type type;
Position position;
};
bool expectMatchAndConsume(char value, const MatchLexeme& begin, bool searchForMissing = false);
void expectMatchAndConsumeFail(Lexeme::Type type, const MatchLexeme& begin, const char* extra = nullptr);
bool expectMatchAndConsumeRecover(char value, const MatchLexeme& begin, bool searchForMissing);
bool expectMatchEndAndConsume(Lexeme::Type type, const MatchLexeme& begin);
void expectMatchEndAndConsumeFail(Lexeme::Type type, const MatchLexeme& begin);
template<typename T>
AstArray<T> copy(const T* data, std::size_t size);
@ -286,6 +300,9 @@ private:
AstTypeError* reportTypeAnnotationError(const Location& location, const AstArray<AstType*>& types, bool isMissing, const char* format, ...)
LUAU_PRINTF_ATTR(5, 6);
AstExpr* reportFunctionArgsError(AstExpr* func, bool self);
void reportAmbiguousCallError();
void nextLexeme();
struct Function
@ -353,7 +370,7 @@ private:
AstName nameError;
AstName nameNil;
Lexeme endMismatchSuspect;
MatchLexeme endMismatchSuspect;
std::vector<Function> functionStack;

View file

@ -14,8 +14,6 @@
LUAU_FASTINTVARIABLE(LuauRecursionLimit, 1000)
LUAU_FASTINTVARIABLE(LuauParseErrorLimit, 100)
LUAU_FASTFLAGVARIABLE(LuauParserFunctionKeywordAsTypeHelp, false)
LUAU_FASTFLAGVARIABLE(LuauFixNamedFunctionParse, false)
LUAU_DYNAMIC_FASTFLAGVARIABLE(LuaReportParseWrongNamedType, false)
@ -181,7 +179,7 @@ Parser::Parser(const char* buffer, size_t bufferSize, AstNameTable& names, Alloc
, lexer(buffer, bufferSize, names)
, allocator(allocator)
, recursionCounter(0)
, endMismatchSuspect(Location(), Lexeme::Eof)
, endMismatchSuspect(Lexeme(Location(), Lexeme::Eof))
, localMap(AstName())
{
Function top;
@ -661,7 +659,7 @@ AstStat* Parser::parseFunctionStat()
matchRecoveryStopOnToken[Lexeme::ReservedEnd]++;
AstExprFunction* body = parseFunctionBody(hasself, matchFunction, debugname, {}).first;
AstExprFunction* body = parseFunctionBody(hasself, matchFunction, debugname, nullptr).first;
matchRecoveryStopOnToken[Lexeme::ReservedEnd]--;
@ -690,7 +688,7 @@ AstStat* Parser::parseLocal()
matchRecoveryStopOnToken[Lexeme::ReservedEnd]++;
auto [body, var] = parseFunctionBody(false, matchFunction, name.name, name);
auto [body, var] = parseFunctionBody(false, matchFunction, name.name, &name);
matchRecoveryStopOnToken[Lexeme::ReservedEnd]--;
@ -782,7 +780,7 @@ AstDeclaredClassProp Parser::parseDeclaredClassMethod()
genericPacks.size = 0;
genericPacks.data = nullptr;
Lexeme matchParen = lexer.current();
MatchLexeme matchParen = lexer.current();
expectAndConsume('(', "function parameter list start");
TempVector<Binding> args(scratchBinding);
@ -838,7 +836,7 @@ AstStat* Parser::parseDeclaration(const Location& start)
auto [generics, genericPacks] = parseGenericTypeList(/* withDefaultValues= */ false);
Lexeme matchParen = lexer.current();
MatchLexeme matchParen = lexer.current();
expectAndConsume('(', "global function declaration");
@ -974,13 +972,13 @@ AstStat* Parser::parseCompoundAssignment(AstExpr* initial, AstExprBinary::Op op)
// funcbody ::= `(' [parlist] `)' [`:' ReturnType] block end
// parlist ::= bindinglist [`,' `...'] | `...'
std::pair<AstExprFunction*, AstLocal*> Parser::parseFunctionBody(
bool hasself, const Lexeme& matchFunction, const AstName& debugname, std::optional<Name> localName)
bool hasself, const Lexeme& matchFunction, const AstName& debugname, const Name* localName)
{
Location start = matchFunction.location;
auto [generics, genericPacks] = parseGenericTypeList(/* withDefaultValues= */ false);
Lexeme matchParen = lexer.current();
MatchLexeme matchParen = lexer.current();
expectAndConsume('(', "function");
TempVector<Binding> args(scratchBinding);
@ -992,7 +990,7 @@ std::pair<AstExprFunction*, AstLocal*> Parser::parseFunctionBody(
std::tie(vararg, varargAnnotation) = parseBindingList(args, /* allowDot3= */ true);
std::optional<Location> argLocation = matchParen.type == Lexeme::Type('(') && lexer.current().type == Lexeme::Type(')')
? std::make_optional(Location(matchParen.location.begin, lexer.current().location.end))
? std::make_optional(Location(matchParen.position, lexer.current().location.end))
: std::nullopt;
expectMatchAndConsume(')', matchParen, true);
@ -1259,7 +1257,7 @@ AstType* Parser::parseTableTypeAnnotation()
Location start = lexer.current().location;
Lexeme matchBrace = lexer.current();
MatchLexeme matchBrace = lexer.current();
expectAndConsume('{', "table type");
while (lexer.current().type != '}')
@ -1638,7 +1636,7 @@ AstTypeOrPack Parser::parseSimpleTypeAnnotation(bool allowPack)
{
return parseFunctionTypeAnnotation(allowPack);
}
else if (FFlag::LuauParserFunctionKeywordAsTypeHelp && lexer.current().type == Lexeme::ReservedFunction)
else if (lexer.current().type == Lexeme::ReservedFunction)
{
Location location = lexer.current().location;
@ -1922,14 +1920,14 @@ AstExpr* Parser::parsePrefixExpr()
{
if (lexer.current().type == '(')
{
Location start = lexer.current().location;
Position start = lexer.current().location.begin;
Lexeme matchParen = lexer.current();
MatchLexeme matchParen = lexer.current();
nextLexeme();
AstExpr* expr = parseExpr();
Location end = lexer.current().location;
Position end = lexer.current().location.end;
if (lexer.current().type != ')')
{
@ -1937,7 +1935,7 @@ AstExpr* Parser::parsePrefixExpr()
expectMatchAndConsumeFail(static_cast<Lexeme::Type>(')'), matchParen, suggestion);
end = lexer.previousLocation();
end = lexer.previousLocation().end;
}
else
{
@ -1955,7 +1953,7 @@ AstExpr* Parser::parsePrefixExpr()
// primaryexp -> prefixexp { `.' NAME | `[' exp `]' | `:' NAME funcargs | funcargs }
AstExpr* Parser::parsePrimaryExpr(bool asStatement)
{
Location start = lexer.current().location;
Position start = lexer.current().location.begin;
AstExpr* expr = parsePrefixExpr();
@ -1970,16 +1968,16 @@ AstExpr* Parser::parsePrimaryExpr(bool asStatement)
Name index = parseIndexName(nullptr, opPosition);
expr = allocator.alloc<AstExprIndexName>(Location(start, index.location), expr, index.name, index.location, opPosition, '.');
expr = allocator.alloc<AstExprIndexName>(Location(start, index.location.end), expr, index.name, index.location, opPosition, '.');
}
else if (lexer.current().type == '[')
{
Lexeme matchBracket = lexer.current();
MatchLexeme matchBracket = lexer.current();
nextLexeme();
AstExpr* index = parseExpr();
Location end = lexer.current().location;
Position end = lexer.current().location.end;
expectMatchAndConsume(']', matchBracket);
@ -1991,23 +1989,20 @@ AstExpr* Parser::parsePrimaryExpr(bool asStatement)
nextLexeme();
Name index = parseIndexName("method name", opPosition);
AstExpr* func = allocator.alloc<AstExprIndexName>(Location(start, index.location), expr, index.name, index.location, opPosition, ':');
AstExpr* func = allocator.alloc<AstExprIndexName>(Location(start, index.location.end), expr, index.name, index.location, opPosition, ':');
expr = parseFunctionArgs(func, true, index.location);
expr = parseFunctionArgs(func, true);
}
else if (lexer.current().type == '(')
{
// This error is handled inside 'parseFunctionArgs' as well, but for better error recovery we need to break out the current loop here
if (!asStatement && expr->location.end.line != lexer.current().location.begin.line)
{
report(lexer.current().location,
"Ambiguous syntax: this looks like an argument list for a function call, but could also be a start of "
"new statement; use ';' to separate statements");
reportAmbiguousCallError();
break;
}
expr = parseFunctionArgs(expr, false, Location());
expr = parseFunctionArgs(expr, false);
}
else if (
lexer.current().type == '{'
@ -2017,7 +2012,7 @@ AstExpr* Parser::parsePrimaryExpr(bool asStatement)
|| lexer.current().type == Lexeme::InterpStringSimple
)
{
expr = parseFunctionArgs(expr, false, Location());
expr = parseFunctionArgs(expr, false);
}
else
{
@ -2172,7 +2167,7 @@ static ConstantNumberParseResult parseInteger(double& result, const char* data,
return ConstantNumberParseResult::Ok;
}
static ConstantNumberParseResult parseNumber(double& result, const char* data)
static ConstantNumberParseResult parseDouble(double& result, const char* data)
{
LUAU_ASSERT(FFlag::LuauLintParseIntegerIssues);
@ -2230,61 +2225,11 @@ AstExpr* Parser::parseSimpleExpr()
Lexeme matchFunction = lexer.current();
nextLexeme();
return parseFunctionBody(false, matchFunction, AstName(), {}).first;
return parseFunctionBody(false, matchFunction, AstName(), nullptr).first;
}
else if (lexer.current().type == Lexeme::Number)
{
scratchData.assign(lexer.current().data, lexer.current().length);
// Remove all internal _ - they don't hold any meaning and this allows parsing code to just pass the string pointer to strtod et al
if (scratchData.find('_') != std::string::npos)
{
scratchData.erase(std::remove(scratchData.begin(), scratchData.end(), '_'), scratchData.end());
}
if (FFlag::LuauLintParseIntegerIssues)
{
double value = 0;
ConstantNumberParseResult result = parseNumber(value, scratchData.c_str());
nextLexeme();
if (result == ConstantNumberParseResult::Malformed)
return reportExprError(start, {}, "Malformed number");
return allocator.alloc<AstExprConstantNumber>(start, value, result);
}
else if (DFFlag::LuaReportParseIntegerIssues)
{
double value = 0;
if (const char* error = parseNumber_DEPRECATED2(value, scratchData.c_str()))
{
nextLexeme();
return reportExprError(start, {}, "%s", error);
}
else
{
nextLexeme();
return allocator.alloc<AstExprConstantNumber>(start, value);
}
}
else
{
double value = 0;
if (parseNumber_DEPRECATED(value, scratchData.c_str()))
{
nextLexeme();
return allocator.alloc<AstExprConstantNumber>(start, value);
}
else
{
nextLexeme();
return reportExprError(start, {}, "Malformed number");
}
}
return parseNumber();
}
else if (lexer.current().type == Lexeme::RawString || lexer.current().type == Lexeme::QuotedString || (FFlag::LuauInterpolatedStringBaseSupport && lexer.current().type == Lexeme::InterpStringSimple))
{
@ -2334,18 +2279,15 @@ AstExpr* Parser::parseSimpleExpr()
}
// args ::= `(' [explist] `)' | tableconstructor | String | InterpString
AstExpr* Parser::parseFunctionArgs(AstExpr* func, bool self, const Location& selfLocation)
AstExpr* Parser::parseFunctionArgs(AstExpr* func, bool self)
{
if (lexer.current().type == '(')
{
Position argStart = lexer.current().location.end;
if (func->location.end.line != lexer.current().location.begin.line)
{
report(lexer.current().location, "Ambiguous syntax: this looks like an argument list for a function call, but could also be a start of "
"new statement; use ';' to separate statements");
}
reportAmbiguousCallError();
Lexeme matchParen = lexer.current();
MatchLexeme matchParen = lexer.current();
nextLexeme();
TempVector<AstExpr*> args(scratchExpr);
@ -2385,18 +2327,29 @@ AstExpr* Parser::parseFunctionArgs(AstExpr* func, bool self, const Location& sel
}
else
{
if (self && lexer.current().location.begin.line != func->location.end.line)
{
return reportExprError(func->location, copy({func}), "Expected function call arguments after '('");
}
else
{
return reportExprError(Location(func->location.begin, lexer.current().location.begin), copy({func}),
"Expected '(', '{' or <string> when parsing function call, got %s", lexer.current().toString().c_str());
}
return reportFunctionArgsError(func, self);
}
}
LUAU_NOINLINE AstExpr* Parser::reportFunctionArgsError(AstExpr* func, bool self)
{
if (self && lexer.current().location.begin.line != func->location.end.line)
{
return reportExprError(func->location, copy({func}), "Expected function call arguments after '('");
}
else
{
return reportExprError(Location(func->location.begin, lexer.current().location.begin), copy({func}),
"Expected '(', '{' or <string> when parsing function call, got %s", lexer.current().toString().c_str());
}
}
LUAU_NOINLINE void Parser::reportAmbiguousCallError()
{
report(lexer.current().location, "Ambiguous syntax: this looks like an argument list for a function call, but could also be a start of "
"new statement; use ';' to separate statements");
}
// tableconstructor ::= `{' [fieldlist] `}'
// fieldlist ::= field {fieldsep field} [fieldsep]
// field ::= `[' exp `]' `=' exp | Name `=' exp | exp
@ -2407,14 +2360,14 @@ AstExpr* Parser::parseTableConstructor()
Location start = lexer.current().location;
Lexeme matchBrace = lexer.current();
MatchLexeme matchBrace = lexer.current();
expectAndConsume('{', "table literal");
while (lexer.current().type != '}')
{
if (lexer.current().type == '[')
{
Lexeme matchLocationBracket = lexer.current();
MatchLexeme matchLocationBracket = lexer.current();
nextLexeme();
AstExpr* key = parseExpr();
@ -2789,6 +2742,63 @@ AstExpr* Parser::parseInterpString()
} while (true);
}
AstExpr* Parser::parseNumber()
{
Location start = lexer.current().location;
scratchData.assign(lexer.current().data, lexer.current().length);
// Remove all internal _ - they don't hold any meaning and this allows parsing code to just pass the string pointer to strtod et al
if (scratchData.find('_') != std::string::npos)
{
scratchData.erase(std::remove(scratchData.begin(), scratchData.end(), '_'), scratchData.end());
}
if (FFlag::LuauLintParseIntegerIssues)
{
double value = 0;
ConstantNumberParseResult result = parseDouble(value, scratchData.c_str());
nextLexeme();
if (result == ConstantNumberParseResult::Malformed)
return reportExprError(start, {}, "Malformed number");
return allocator.alloc<AstExprConstantNumber>(start, value, result);
}
else if (DFFlag::LuaReportParseIntegerIssues)
{
double value = 0;
if (const char* error = parseNumber_DEPRECATED2(value, scratchData.c_str()))
{
nextLexeme();
return reportExprError(start, {}, "%s", error);
}
else
{
nextLexeme();
return allocator.alloc<AstExprConstantNumber>(start, value);
}
}
else
{
double value = 0;
if (parseNumber_DEPRECATED(value, scratchData.c_str()))
{
nextLexeme();
return allocator.alloc<AstExprConstantNumber>(start, value);
}
else
{
nextLexeme();
return reportExprError(start, {}, "Malformed number");
}
}
}
AstLocal* Parser::pushLocal(const Binding& binding)
{
const Name& name = binding.name;
@ -2860,7 +2870,7 @@ LUAU_NOINLINE void Parser::expectAndConsumeFail(Lexeme::Type type, const char* c
report(lexer.current().location, "Expected %s, got %s", typeString.c_str(), currLexemeString.c_str());
}
bool Parser::expectMatchAndConsume(char value, const Lexeme& begin, bool searchForMissing)
bool Parser::expectMatchAndConsume(char value, const MatchLexeme& begin, bool searchForMissing)
{
Lexeme::Type type = static_cast<Lexeme::Type>(static_cast<unsigned char>(value));
@ -2868,42 +2878,7 @@ bool Parser::expectMatchAndConsume(char value, const Lexeme& begin, bool searchF
{
expectMatchAndConsumeFail(type, begin);
if (searchForMissing)
{
// previous location is taken because 'current' lexeme is already the next token
unsigned currentLine = lexer.previousLocation().end.line;
// search to the end of the line for expected token
// we will also stop if we hit a token that can be handled by parsing function above the current one
Lexeme::Type lexemeType = lexer.current().type;
while (currentLine == lexer.current().location.begin.line && lexemeType != type && matchRecoveryStopOnToken[lexemeType] == 0)
{
nextLexeme();
lexemeType = lexer.current().type;
}
if (lexemeType == type)
{
nextLexeme();
return true;
}
}
else
{
// check if this is an extra token and the expected token is next
if (lexer.lookahead().type == type)
{
// skip invalid and consume expected
nextLexeme();
nextLexeme();
return true;
}
}
return false;
return expectMatchAndConsumeRecover(value, begin, searchForMissing);
}
else
{
@ -2913,21 +2888,64 @@ bool Parser::expectMatchAndConsume(char value, const Lexeme& begin, bool searchF
}
}
// LUAU_NOINLINE is used to limit the stack cost of this function due to std::string objects, and to increase caller performance since this code is
// cold
LUAU_NOINLINE void Parser::expectMatchAndConsumeFail(Lexeme::Type type, const Lexeme& begin, const char* extra)
LUAU_NOINLINE bool Parser::expectMatchAndConsumeRecover(char value, const MatchLexeme& begin, bool searchForMissing)
{
std::string typeString = Lexeme(Location(Position(0, 0), 0), type).toString();
Lexeme::Type type = static_cast<Lexeme::Type>(static_cast<unsigned char>(value));
if (lexer.current().location.begin.line == begin.location.begin.line)
report(lexer.current().location, "Expected %s (to close %s at column %d), got %s%s", typeString.c_str(), begin.toString().c_str(),
begin.location.begin.column + 1, lexer.current().toString().c_str(), extra ? extra : "");
if (searchForMissing)
{
// previous location is taken because 'current' lexeme is already the next token
unsigned currentLine = lexer.previousLocation().end.line;
// search to the end of the line for expected token
// we will also stop if we hit a token that can be handled by parsing function above the current one
Lexeme::Type lexemeType = lexer.current().type;
while (currentLine == lexer.current().location.begin.line && lexemeType != type && matchRecoveryStopOnToken[lexemeType] == 0)
{
nextLexeme();
lexemeType = lexer.current().type;
}
if (lexemeType == type)
{
nextLexeme();
return true;
}
}
else
report(lexer.current().location, "Expected %s (to close %s at line %d), got %s%s", typeString.c_str(), begin.toString().c_str(),
begin.location.begin.line + 1, lexer.current().toString().c_str(), extra ? extra : "");
{
// check if this is an extra token and the expected token is next
if (lexer.lookahead().type == type)
{
// skip invalid and consume expected
nextLexeme();
nextLexeme();
return true;
}
}
return false;
}
bool Parser::expectMatchEndAndConsume(Lexeme::Type type, const Lexeme& begin)
// LUAU_NOINLINE is used to limit the stack cost of this function due to std::string objects, and to increase caller performance since this code is
// cold
LUAU_NOINLINE void Parser::expectMatchAndConsumeFail(Lexeme::Type type, const MatchLexeme& begin, const char* extra)
{
std::string typeString = Lexeme(Location(Position(0, 0), 0), type).toString();
std::string matchString = Lexeme(Location(Position(0, 0), 0), begin.type).toString();
if (lexer.current().location.begin.line == begin.position.line)
report(lexer.current().location, "Expected %s (to close %s at column %d), got %s%s", typeString.c_str(), matchString.c_str(),
begin.position.column + 1, lexer.current().toString().c_str(), extra ? extra : "");
else
report(lexer.current().location, "Expected %s (to close %s at line %d), got %s%s", typeString.c_str(), matchString.c_str(),
begin.position.line + 1, lexer.current().toString().c_str(), extra ? extra : "");
}
bool Parser::expectMatchEndAndConsume(Lexeme::Type type, const MatchLexeme& begin)
{
if (lexer.current().type != type)
{
@ -2949,9 +2967,9 @@ bool Parser::expectMatchEndAndConsume(Lexeme::Type type, const Lexeme& begin)
{
// If the token matches on a different line and a different column, it suggests misleading indentation
// This can be used to pinpoint the problem location for a possible future *actual* mismatch
if (lexer.current().location.begin.line != begin.location.begin.line &&
lexer.current().location.begin.column != begin.location.begin.column &&
endMismatchSuspect.location.begin.line < begin.location.begin.line) // Only replace the previous suspect with more recent suspects
if (lexer.current().location.begin.line != begin.position.line &&
lexer.current().location.begin.column != begin.position.column &&
endMismatchSuspect.position.line < begin.position.line) // Only replace the previous suspect with more recent suspects
{
endMismatchSuspect = begin;
}
@ -2964,12 +2982,12 @@ bool Parser::expectMatchEndAndConsume(Lexeme::Type type, const Lexeme& begin)
// LUAU_NOINLINE is used to limit the stack cost of this function due to std::string objects, and to increase caller performance since this code is
// cold
LUAU_NOINLINE void Parser::expectMatchEndAndConsumeFail(Lexeme::Type type, const Lexeme& begin)
LUAU_NOINLINE void Parser::expectMatchEndAndConsumeFail(Lexeme::Type type, const MatchLexeme& begin)
{
if (endMismatchSuspect.type != Lexeme::Eof && endMismatchSuspect.location.begin.line > begin.location.begin.line)
if (endMismatchSuspect.type != Lexeme::Eof && endMismatchSuspect.position.line > begin.position.line)
{
std::string suggestion =
format("; did you forget to close %s at line %d?", endMismatchSuspect.toString().c_str(), endMismatchSuspect.location.begin.line + 1);
std::string matchString = Lexeme(Location(Position(0, 0), 0), endMismatchSuspect.type).toString();
std::string suggestion = format("; did you forget to close %s at line %d?", matchString.c_str(), endMismatchSuspect.position.line + 1);
expectMatchAndConsumeFail(type, begin, suggestion.c_str());
}

511
CLI/Reduce.cpp Normal file
View file

@ -0,0 +1,511 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#include "Luau/Ast.h"
#include "Luau/Common.h"
#include "Luau/Parser.h"
#include "Luau/Transpiler.h"
#include "FileUtils.h"
#include <algorithm>
#include <stdio.h>
#include <string>
#include <string_view>
#include <queue>
#define VERBOSE 0 // 1 - print out commandline invocations. 2 - print out stdout
#ifdef _WIN32
const auto popen = &_popen;
const auto pclose = &_pclose;
#endif
using namespace Luau;
enum class TestResult
{
BugFound, // We encountered the bug we are trying to isolate
NoBug, // We did not encounter the bug we are trying to isolate
};
struct Enqueuer : public AstVisitor
{
std::queue<AstStatBlock*>* queue;
explicit Enqueuer(std::queue<AstStatBlock*>* queue)
: queue(queue)
{
LUAU_ASSERT(queue);
}
bool visit(AstStatBlock* block) override
{
queue->push(block);
return false;
}
};
struct Reducer
{
Allocator allocator;
AstNameTable nameTable{allocator};
ParseOptions parseOptions;
ParseResult parseResult;
AstStatBlock* root;
std::string tempScriptName;
std::string appName;
std::vector<std::string> appArgs;
std::string_view searchText;
Reducer()
{
parseOptions.captureComments = true;
}
std::string readLine(FILE* f)
{
std::string line = "";
char buffer[256];
while (fgets(buffer, sizeof(buffer), f))
{
auto len = strlen(buffer);
line += std::string(buffer, len);
if (buffer[len - 1] == '\n')
break;
}
return line;
}
void writeTempScript(bool minify = false)
{
std::string source = transpileWithTypes(*root);
if (minify)
{
size_t pos = 0;
do
{
pos = source.find("\n\n", pos);
if (pos == std::string::npos)
break;
source.erase(pos, 1);
} while (true);
}
FILE* f = fopen(tempScriptName.c_str(), "w");
if (!f)
{
printf("Unable to open temp script to %s\n", tempScriptName.c_str());
exit(2);
}
for (const HotComment& comment : parseResult.hotcomments)
fprintf(f, "--!%s\n", comment.content.c_str());
auto written = fwrite(source.data(), 1, source.size(), f);
if (written != source.size())
{
printf("??? %zu %zu\n", written, source.size());
printf("Unable to write to temp script %s\n", tempScriptName.c_str());
exit(3);
}
fclose(f);
}
int step = 0;
std::string escape(const std::string& s)
{
std::string result;
result.reserve(s.size() + 20); // guess
result += '"';
for (char c : s)
{
if (c == '"')
result += '\\';
result += c;
}
result += '"';
return result;
}
TestResult run()
{
writeTempScript();
std::string command = appName + " " + escape(tempScriptName);
for (const auto& arg : appArgs)
command += " " + escape(arg);
#if VERBOSE >= 1
printf("running %s\n", command.c_str());
#endif
TestResult result = TestResult::NoBug;
++step;
printf("Step %4d...\n", step);
FILE* p = popen(command.c_str(), "r");
while (!feof(p))
{
std::string s = readLine(p);
#if VERBOSE >= 2
printf("%s", s.c_str());
#endif
if (std::string::npos != s.find(searchText))
{
result = TestResult::BugFound;
break;
}
}
pclose(p);
return result;
}
std::vector<AstStat*> getNestedStats(AstStat* stat)
{
std::vector<AstStat*> result;
auto append = [&](AstStatBlock* block) {
if (block)
result.insert(result.end(), block->body.data, block->body.data + block->body.size);
};
if (auto block = stat->as<AstStatBlock>())
append(block);
else if (auto ifs = stat->as<AstStatIf>())
{
append(ifs->thenbody);
if (ifs->elsebody)
{
if (AstStatBlock* elseBlock = ifs->elsebody->as<AstStatBlock>())
append(elseBlock);
else if (AstStatIf* elseIf = ifs->elsebody->as<AstStatIf>())
{
auto innerStats = getNestedStats(elseIf);
result.insert(end(result), begin(innerStats), end(innerStats));
}
else
{
printf("AstStatIf's else clause can have more statement types than I thought\n");
LUAU_ASSERT(0);
}
}
}
else if (auto w = stat->as<AstStatWhile>())
append(w->body);
else if (auto r = stat->as<AstStatRepeat>())
append(r->body);
else if (auto f = stat->as<AstStatFor>())
append(f->body);
else if (auto f = stat->as<AstStatForIn>())
append(f->body);
else if (auto f = stat->as<AstStatFunction>())
append(f->func->body);
else if (auto f = stat->as<AstStatLocalFunction>())
append(f->func->body);
return result;
}
// Move new body data into allocator-managed storage so that it's safe to keep around longterm.
AstStat** reallocateStatements(const std::vector<AstStat*>& statements)
{
AstStat** newData = static_cast<AstStat**>(allocator.allocate(sizeof(AstStat*) * statements.size()));
std::copy(statements.data(), statements.data() + statements.size(), newData);
return newData;
}
// Semiopen interval
using Span = std::pair<size_t, size_t>;
// Generates 'chunks' semiopen spans of equal-ish size to span the indeces running from 0 to 'size'
// Also inverses.
std::vector<std::pair<Span, Span>> generateSpans(size_t size, size_t chunks)
{
if (size <= 1)
return {};
LUAU_ASSERT(chunks > 0);
size_t chunkLength = std::max<size_t>(1, size / chunks);
std::vector<std::pair<Span, Span>> result;
auto append = [&result](Span a, Span b) {
if (a.first == a.second && b.first == b.second)
return;
else
result.emplace_back(a, b);
};
size_t i = 0;
while (i < size)
{
size_t end = std::min(i + chunkLength, size);
append(Span{0, i}, Span{end, size});
i = end;
}
i = 0;
while (i < size)
{
size_t end = std::min(i + chunkLength, size);
append(Span{i, end}, Span{size, size});
i = end;
}
return result;
}
// Returns the statements of block within span1 and span2
// Also has the hokey restriction that span1 must come before span2
std::vector<AstStat*> prunedSpan(AstStatBlock* block, Span span1, Span span2)
{
std::vector<AstStat*> result;
for (size_t i = span1.first; i < span1.second; ++i)
result.push_back(block->body.data[i]);
for (size_t i = span2.first; i < span2.second; ++i)
result.push_back(block->body.data[i]);
return result;
}
// returns true if anything was culled plus the chunk count
std::pair<bool, size_t> deleteChildStatements(AstStatBlock* block, size_t chunkCount)
{
if (block->body.size == 0)
return {false, chunkCount};
do
{
auto permutations = generateSpans(block->body.size, chunkCount);
for (const auto& [span1, span2] : permutations)
{
auto tempStatements = prunedSpan(block, span1, span2);
AstArray<AstStat*> backupBody{tempStatements.data(), tempStatements.size()};
std::swap(block->body, backupBody);
TestResult result = run();
if (result == TestResult::BugFound)
{
// The bug still reproduces without the statements we've culled. Commit.
block->body.data = reallocateStatements(tempStatements);
return {true, std::max<size_t>(2, chunkCount - 1)};
}
else
{
// The statements we've culled are critical for the reproduction of the bug.
// TODO try promoting its contents into this scope
std::swap(block->body, backupBody);
}
}
chunkCount *= 2;
} while (chunkCount <= block->body.size);
return {false, block->body.size};
}
bool deleteChildStatements(AstStatBlock* b)
{
bool result = false;
size_t chunkCount = 2;
while (true)
{
auto [workDone, newChunkCount] = deleteChildStatements(b, chunkCount);
if (workDone)
{
result = true;
chunkCount = newChunkCount;
continue;
}
else
break;
}
return result;
}
bool tryPromotingChildStatements(AstStatBlock* b, size_t index)
{
std::vector<AstStat*> tempStats(b->body.data, b->body.data + b->body.size);
AstStat* removed = tempStats.at(index);
tempStats.erase(begin(tempStats) + index);
std::vector<AstStat*> nestedStats = getNestedStats(removed);
tempStats.insert(begin(tempStats) + index, begin(nestedStats), end(nestedStats));
AstArray<AstStat*> tempArray{tempStats.data(), tempStats.size()};
std::swap(b->body, tempArray);
TestResult result = run();
if (result == TestResult::BugFound)
{
b->body.data = reallocateStatements(tempStats);
return true;
}
else
{
std::swap(b->body, tempArray);
return false;
}
}
// We live with some weirdness because I'm kind of lazy: If a statement's
// contents are promoted, we try promoting those prometed statements right
// away. I don't think it matters: If we can delete a statement and still
// exhibit the bug, we should do so. The order isn't so important.
bool tryPromotingChildStatements(AstStatBlock* b)
{
size_t i = 0;
while (i < b->body.size)
{
bool promoted = tryPromotingChildStatements(b, i);
if (!promoted)
++i;
}
return false;
}
void walk(AstStatBlock* block)
{
std::queue<AstStatBlock*> queue;
Enqueuer enqueuer{&queue};
queue.push(block);
while (!queue.empty())
{
AstStatBlock* b = queue.front();
queue.pop();
bool result = false;
do
{
result = deleteChildStatements(b);
/* Try other reductions here before we walk into child statements
* Other reductions to try someday:
*
* Promoting a statement's children to the enclosing block.
* Deleting type annotations
* Deleting parts of type annotations
* Replacing subexpressions with ({} :: any)
* Inlining type aliases
* Inlining constants
* Inlining functions
*/
result |= tryPromotingChildStatements(b);
} while (result);
for (AstStat* stat : b->body)
stat->visit(&enqueuer);
}
}
void run(const std::string scriptName, const std::string appName, const std::vector<std::string>& appArgs, std::string_view source,
std::string_view searchText)
{
tempScriptName = scriptName;
if (tempScriptName.substr(tempScriptName.size() - 4) == ".lua")
{
tempScriptName.erase(tempScriptName.size() - 4);
tempScriptName += "-reduced.lua";
}
else
{
this->tempScriptName = scriptName + "-reduced";
}
#if 0
// Handy debugging trick: VS Code will update its view of the file in realtime as it is edited.
std::string wheee = "code " + tempScriptName;
system(wheee.c_str());
#endif
printf("Temp script: %s\n", tempScriptName.c_str());
this->appName = appName;
this->appArgs = appArgs;
this->searchText = searchText;
parseResult = Parser::parse(source.data(), source.size(), nameTable, allocator, parseOptions);
if (!parseResult.errors.empty())
{
printf("Parse errors\n");
exit(1);
}
root = parseResult.root;
const TestResult initialResult = run();
if (initialResult == TestResult::NoBug)
{
printf("Could not find failure string in the unmodified script! Check your commandline arguments\n");
exit(2);
}
walk(root);
writeTempScript(/* minify */ true);
printf("Done! Check %s\n", tempScriptName.c_str());
}
};
[[noreturn]] void help(const std::vector<std::string_view>& args)
{
printf("Syntax: %s script application \"search text\" [arguments]\n", args[0].data());
exit(1);
}
int main(int argc, char** argv)
{
const std::vector<std::string_view> args(argv, argv + argc);
if (args.size() < 4)
help(args);
for (int i = 1; i < args.size(); ++i)
{
if (args[i] == "--help")
help(args);
}
const std::string scriptName = argv[1];
const std::string appName = argv[2];
const std::string searchText = argv[3];
const std::vector<std::string> appArgs(begin(args) + 4, end(args));
std::optional<std::string> source = readFile(scriptName);
if (!source)
{
printf("Could not read source %s\n", argv[1]);
exit(1);
}
Reducer reducer;
reducer.run(scriptName, appName, appArgs, *source, searchText);
}

View file

@ -32,11 +32,13 @@ if(LUAU_BUILD_CLI)
add_executable(Luau.Repl.CLI)
add_executable(Luau.Analyze.CLI)
add_executable(Luau.Ast.CLI)
add_executable(Luau.Reduce.CLI)
# This also adds target `name` on Linux/macOS and `name.exe` on Windows
set_target_properties(Luau.Repl.CLI PROPERTIES OUTPUT_NAME luau)
set_target_properties(Luau.Analyze.CLI PROPERTIES OUTPUT_NAME luau-analyze)
set_target_properties(Luau.Ast.CLI PROPERTIES OUTPUT_NAME luau-ast)
set_target_properties(Luau.Reduce.CLI PROPERTIES OUTPUT_NAME luau-reduce)
endif()
if(LUAU_BUILD_TESTS)
@ -49,6 +51,7 @@ if(LUAU_BUILD_WEB)
add_executable(Luau.Web)
endif()
include(Sources.cmake)
target_include_directories(Luau.Common INTERFACE Common/include)
@ -171,6 +174,10 @@ if(LUAU_BUILD_CLI)
target_link_libraries(Luau.Analyze.CLI PRIVATE Luau.Analysis)
target_link_libraries(Luau.Ast.CLI PRIVATE Luau.Ast Luau.Analysis)
target_compile_features(Luau.Reduce.CLI PRIVATE cxx_std_17)
target_include_directories(Luau.Reduce.CLI PUBLIC Reduce/include)
target_link_libraries(Luau.Reduce.CLI PRIVATE Luau.Common Luau.Ast Luau.Analysis)
endif()
if(LUAU_BUILD_TESTS)

View file

@ -38,14 +38,21 @@ public:
// Two operand mov instruction has additional specialized encodings
void mov(OperandX64 lhs, OperandX64 rhs);
void mov64(RegisterX64 lhs, int64_t imm);
void movsx(RegisterX64 lhs, OperandX64 rhs);
void movzx(RegisterX64 lhs, OperandX64 rhs);
// Base one operand instruction with 2 opcode selection
void div(OperandX64 op);
void idiv(OperandX64 op);
void mul(OperandX64 op);
void imul(OperandX64 op);
void neg(OperandX64 op);
void not_(OperandX64 op);
// Additional forms of imul
void imul(OperandX64 lhs, OperandX64 rhs);
void imul(OperandX64 dst, OperandX64 lhs, int32_t rhs);
void test(OperandX64 lhs, OperandX64 rhs);
void lea(OperandX64 lhs, OperandX64 rhs);
@ -76,6 +83,12 @@ public:
void vxorpd(OperandX64 dst, OperandX64 src1, OperandX64 src2);
void vcomisd(OperandX64 src1, OperandX64 src2);
void vucomisd(OperandX64 src1, OperandX64 src2);
void vcvttsd2si(OperandX64 dst, OperandX64 src);
void vcvtsi2sd(OperandX64 dst, OperandX64 src1, OperandX64 src2);
void vroundsd(OperandX64 dst, OperandX64 src1, OperandX64 src2, uint8_t mode);
void vsqrtpd(OperandX64 dst, OperandX64 src);
void vsqrtps(OperandX64 dst, OperandX64 src);
@ -105,6 +118,7 @@ public:
OperandX64 f32(float value);
OperandX64 f64(double value);
OperandX64 f32x4(float x, float y, float z, float w);
OperandX64 bytes(const void* ptr, size_t size, size_t align = 8);
// Resulting data and code that need to be copied over one after the other
// The *end* of 'data' has to be aligned to 16 bytes, this will also align 'code'
@ -130,6 +144,8 @@ private:
void placeAvx(const char* name, OperandX64 dst, OperandX64 src, uint8_t code, bool setW, uint8_t mode, uint8_t prefix);
void placeAvx(const char* name, OperandX64 dst, OperandX64 src, uint8_t code, uint8_t coderev, bool setW, uint8_t mode, uint8_t prefix);
void placeAvx(const char* name, OperandX64 dst, OperandX64 src1, OperandX64 src2, uint8_t code, bool setW, uint8_t mode, uint8_t prefix);
void placeAvx(
const char* name, OperandX64 dst, OperandX64 src1, OperandX64 src2, uint8_t imm8, uint8_t code, bool setW, uint8_t mode, uint8_t prefix);
// Instruction components
void placeRegAndModRegMem(OperandX64 lhs, OperandX64 rhs);
@ -157,6 +173,7 @@ private:
LUAU_NOINLINE void log(const char* opcode, OperandX64 op);
LUAU_NOINLINE void log(const char* opcode, OperandX64 op1, OperandX64 op2);
LUAU_NOINLINE void log(const char* opcode, OperandX64 op1, OperandX64 op2, OperandX64 op3);
LUAU_NOINLINE void log(const char* opcode, OperandX64 op1, OperandX64 op2, OperandX64 op3, OperandX64 op4);
LUAU_NOINLINE void log(Label label);
LUAU_NOINLINE void log(const char* opcode, Label label);
void log(OperandX64 op);

View file

@ -46,6 +46,44 @@ const unsigned AVX_F2 = 0b11;
const unsigned kMaxAlign = 16;
// Utility functions to correctly write data on big endian machines
#if defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__
#include <endian.h>
static void writeu32(uint8_t* target, uint32_t value)
{
value = htole32(value);
memcpy(target, &value, sizeof(value));
}
static void writeu64(uint8_t* target, uint64_t value)
{
value = htole64(value);
memcpy(target, &value, sizeof(value));
}
static void writef32(uint8_t* target, float value)
{
static_assert(sizeof(float) == sizeof(uint32_t), "type size must match to reinterpret data");
uint32_t data;
memcpy(&data, &value, sizeof(value));
writeu32(target, data);
}
static void writef64(uint8_t* target, double value)
{
static_assert(sizeof(double) == sizeof(uint64_t), "type size must match to reinterpret data");
uint64_t data;
memcpy(&data, &value, sizeof(value));
writeu64(target, data);
}
#else
#define writeu32(target, value) memcpy(target, &value, sizeof(value))
#define writeu64(target, value) memcpy(target, &value, sizeof(value))
#define writef32(target, value) memcpy(target, &value, sizeof(value))
#define writef64(target, value) memcpy(target, &value, sizeof(value))
#endif
AssemblyBuilderX64::AssemblyBuilderX64(bool logText)
: logText(logText)
{
@ -195,6 +233,34 @@ void AssemblyBuilderX64::mov64(RegisterX64 lhs, int64_t imm)
commit();
}
void AssemblyBuilderX64::movsx(RegisterX64 lhs, OperandX64 rhs)
{
if (logText)
log("movsx", lhs, rhs);
LUAU_ASSERT(rhs.memSize == SizeX64::byte || rhs.memSize == SizeX64::word);
placeRex(lhs, rhs);
place(0x0f);
place(rhs.memSize == SizeX64::byte ? 0xbe : 0xbf);
placeRegAndModRegMem(lhs, rhs);
commit();
}
void AssemblyBuilderX64::movzx(RegisterX64 lhs, OperandX64 rhs)
{
if (logText)
log("movzx", lhs, rhs);
LUAU_ASSERT(rhs.memSize == SizeX64::byte || rhs.memSize == SizeX64::word);
placeRex(lhs, rhs);
place(0x0f);
place(rhs.memSize == SizeX64::byte ? 0xb6 : 0xb7);
placeRegAndModRegMem(lhs, rhs);
commit();
}
void AssemblyBuilderX64::div(OperandX64 op)
{
placeUnaryModRegMem("div", op, 0xf6, 0xf7, 6);
@ -210,6 +276,11 @@ void AssemblyBuilderX64::mul(OperandX64 op)
placeUnaryModRegMem("mul", op, 0xf6, 0xf7, 4);
}
void AssemblyBuilderX64::imul(OperandX64 op)
{
placeUnaryModRegMem("imul", op, 0xf6, 0xf7, 5);
}
void AssemblyBuilderX64::neg(OperandX64 op)
{
placeUnaryModRegMem("neg", op, 0xf6, 0xf7, 3);
@ -220,6 +291,41 @@ void AssemblyBuilderX64::not_(OperandX64 op)
placeUnaryModRegMem("not", op, 0xf6, 0xf7, 2);
}
void AssemblyBuilderX64::imul(OperandX64 lhs, OperandX64 rhs)
{
if (logText)
log("imul", lhs, rhs);
placeRex(lhs.base, rhs);
place(0x0f);
place(0xaf);
placeRegAndModRegMem(lhs, rhs);
commit();
}
void AssemblyBuilderX64::imul(OperandX64 dst, OperandX64 lhs, int32_t rhs)
{
if (logText)
log("imul", dst, lhs, rhs);
placeRex(dst.base, lhs);
if (int8_t(rhs) == rhs)
{
place(0x6b);
placeRegAndModRegMem(dst, lhs);
placeImm8(rhs);
}
else
{
place(0x69);
placeRegAndModRegMem(dst, lhs);
placeImm32(rhs);
}
commit();
}
void AssemblyBuilderX64::test(OperandX64 lhs, OperandX64 rhs)
{
// No forms for r/m*, imm8 and reg, r/m*
@ -368,6 +474,26 @@ void AssemblyBuilderX64::vcomisd(OperandX64 src1, OperandX64 src2)
placeAvx("vcomisd", src1, src2, 0x2f, false, AVX_0F, AVX_66);
}
void AssemblyBuilderX64::vucomisd(OperandX64 src1, OperandX64 src2)
{
placeAvx("vucomisd", src1, src2, 0x2e, false, AVX_0F, AVX_66);
}
void AssemblyBuilderX64::vcvttsd2si(OperandX64 dst, OperandX64 src)
{
placeAvx("vcvttsd2si", dst, src, 0x2c, dst.base.size == SizeX64::dword, AVX_0F, AVX_F2);
}
void AssemblyBuilderX64::vcvtsi2sd(OperandX64 dst, OperandX64 src1, OperandX64 src2)
{
placeAvx("vcvtsi2sd", dst, src1, src2, 0x2a, (src2.cat == CategoryX64::reg ? src2.base.size : src2.memSize) == SizeX64::dword, AVX_0F, AVX_F2);
}
void AssemblyBuilderX64::vroundsd(OperandX64 dst, OperandX64 src1, OperandX64 src2, uint8_t mode)
{
placeAvx("vroundsd", dst, src1, src2, mode, 0x0b, false, AVX_0F3A, AVX_66);
}
void AssemblyBuilderX64::vsqrtpd(OperandX64 dst, OperandX64 src)
{
placeAvx("vsqrtpd", dst, src, 0x51, false, AVX_0F, AVX_66);
@ -436,7 +562,7 @@ void AssemblyBuilderX64::finalize()
for (Label fixup : pendingLabels)
{
uint32_t value = labelLocations[fixup.id - 1] - (fixup.location + 4);
memcpy(&code[fixup.location], &value, sizeof(value));
writeu32(&code[fixup.location], value);
}
size_t dataSize = data.size() - dataPos;
@ -479,34 +605,41 @@ void AssemblyBuilderX64::setLabel(Label& label)
OperandX64 AssemblyBuilderX64::i64(int64_t value)
{
size_t pos = allocateData(8, 8);
memcpy(&data[pos], &value, sizeof(value));
writeu64(&data[pos], value);
return OperandX64(SizeX64::qword, noreg, 1, rip, int32_t(pos - data.size()));
}
OperandX64 AssemblyBuilderX64::f32(float value)
{
size_t pos = allocateData(4, 4);
memcpy(&data[pos], &value, sizeof(value));
writef32(&data[pos], value);
return OperandX64(SizeX64::dword, noreg, 1, rip, int32_t(pos - data.size()));
}
OperandX64 AssemblyBuilderX64::f64(double value)
{
size_t pos = allocateData(8, 8);
memcpy(&data[pos], &value, sizeof(value));
writef64(&data[pos], value);
return OperandX64(SizeX64::qword, noreg, 1, rip, int32_t(pos - data.size()));
}
OperandX64 AssemblyBuilderX64::f32x4(float x, float y, float z, float w)
{
size_t pos = allocateData(16, 16);
memcpy(&data[pos], &x, sizeof(x));
memcpy(&data[pos + 4], &y, sizeof(y));
memcpy(&data[pos + 8], &z, sizeof(z));
memcpy(&data[pos + 12], &w, sizeof(w));
writef32(&data[pos], x);
writef32(&data[pos + 4], y);
writef32(&data[pos + 8], z);
writef32(&data[pos + 12], w);
return OperandX64(SizeX64::xmmword, noreg, 1, rip, int32_t(pos - data.size()));
}
OperandX64 AssemblyBuilderX64::bytes(const void* ptr, size_t size, size_t align)
{
size_t pos = allocateData(size, align);
memcpy(&data[pos], ptr, size);
return OperandX64(SizeX64::qword, noreg, 1, rip, int32_t(pos - data.size()));
}
void AssemblyBuilderX64::placeBinary(const char* name, OperandX64 lhs, OperandX64 rhs, uint8_t codeimm8, uint8_t codeimm, uint8_t codeimmImm8,
uint8_t code8rev, uint8_t coderev, uint8_t code8, uint8_t code, uint8_t opreg)
{
@ -700,6 +833,24 @@ void AssemblyBuilderX64::placeAvx(
commit();
}
void AssemblyBuilderX64::placeAvx(
const char* name, OperandX64 dst, OperandX64 src1, OperandX64 src2, uint8_t imm8, uint8_t code, bool setW, uint8_t mode, uint8_t prefix)
{
LUAU_ASSERT(dst.cat == CategoryX64::reg);
LUAU_ASSERT(src1.cat == CategoryX64::reg);
LUAU_ASSERT(src2.cat == CategoryX64::reg || src2.cat == CategoryX64::mem);
if (logText)
log(name, dst, src1, src2, imm8);
placeVex(dst, src1, src2, setW, mode, prefix);
place(code);
placeRegAndModRegMem(dst, src2);
placeImm8(imm8);
commit();
}
void AssemblyBuilderX64::placeRex(RegisterX64 op)
{
uint8_t code = REX_W(op.size == SizeX64::qword) | REX_B(op);
@ -861,16 +1012,18 @@ void AssemblyBuilderX64::placeImm8(int32_t imm)
void AssemblyBuilderX64::placeImm32(int32_t imm)
{
LUAU_ASSERT(codePos + sizeof(imm) < codeEnd);
memcpy(codePos, &imm, sizeof(imm));
codePos += sizeof(imm);
uint8_t* pos = codePos;
LUAU_ASSERT(pos + sizeof(imm) < codeEnd);
writeu32(pos, imm);
codePos = pos + sizeof(imm);
}
void AssemblyBuilderX64::placeImm64(int64_t imm)
{
LUAU_ASSERT(codePos + sizeof(imm) < codeEnd);
memcpy(codePos, &imm, sizeof(imm));
codePos += sizeof(imm);
uint8_t* pos = codePos;
LUAU_ASSERT(pos + sizeof(imm) < codeEnd);
writeu64(pos, imm);
codePos = pos + sizeof(imm);
}
void AssemblyBuilderX64::placeLabel(Label& label)
@ -970,6 +1123,19 @@ void AssemblyBuilderX64::log(const char* opcode, OperandX64 op1, OperandX64 op2,
text.append("\n");
}
void AssemblyBuilderX64::log(const char* opcode, OperandX64 op1, OperandX64 op2, OperandX64 op3, OperandX64 op4)
{
logAppend(" %-12s", opcode);
log(op1);
text.append(",");
log(op2);
text.append(",");
log(op3);
text.append(",");
log(op4);
text.append("\n");
}
void AssemblyBuilderX64::log(Label label)
{
logAppend(".L%d:\n", label.id);

View file

@ -515,6 +515,9 @@ enum LuauBuiltinFunction
// rawlen
LBF_RAWLEN,
// bit32.extract(_, k, k)
LBF_BIT32_EXTRACTK,
};
// Capture type, used in LOP_CAPTURE

View file

@ -319,11 +319,12 @@ Constant foldBuiltin(int bfid, const Constant* args, size_t count)
break;
case LBF_BIT32_EXTRACT:
if (count == 3 && args[0].type == Constant::Type_Number && args[1].type == Constant::Type_Number && args[2].type == Constant::Type_Number)
if (count >= 2 && args[0].type == Constant::Type_Number && args[1].type == Constant::Type_Number &&
(count == 2 || args[2].type == Constant::Type_Number))
{
uint32_t u = bit32(args[0].valueNumber);
int f = int(args[1].valueNumber);
int w = int(args[2].valueNumber);
int w = count == 2 ? 1 : int(args[2].valueNumber);
if (f >= 0 && w > 0 && f + w <= 32)
{
@ -356,13 +357,13 @@ Constant foldBuiltin(int bfid, const Constant* args, size_t count)
break;
case LBF_BIT32_REPLACE:
if (count == 4 && args[0].type == Constant::Type_Number && args[1].type == Constant::Type_Number && args[2].type == Constant::Type_Number &&
args[3].type == Constant::Type_Number)
if (count >= 3 && args[0].type == Constant::Type_Number && args[1].type == Constant::Type_Number && args[2].type == Constant::Type_Number &&
(count == 3 || args[3].type == Constant::Type_Number))
{
uint32_t n = bit32(args[0].valueNumber);
uint32_t v = bit32(args[1].valueNumber);
int f = int(args[2].valueNumber);
int w = int(args[3].valueNumber);
int w = count == 3 ? 1 : int(args[3].valueNumber);
if (f >= 0 && w > 0 && f + w <= 32)
{

View file

@ -24,13 +24,14 @@ LUAU_FASTINTVARIABLE(LuauCompileInlineThreshold, 25)
LUAU_FASTINTVARIABLE(LuauCompileInlineThresholdMaxBoost, 300)
LUAU_FASTINTVARIABLE(LuauCompileInlineDepth, 5)
LUAU_FASTFLAGVARIABLE(LuauCompileNoIpairs, false)
LUAU_FASTFLAGVARIABLE(LuauCompileFreeReassign, false)
LUAU_FASTFLAGVARIABLE(LuauCompileXEQ, false)
LUAU_FASTFLAG(LuauInterpolatedStringBaseSupport)
LUAU_FASTFLAGVARIABLE(LuauCompileOptimalAssignment, false)
LUAU_FASTFLAGVARIABLE(LuauCompileExtractK, false)
namespace Luau
{
@ -40,6 +41,8 @@ static const uint32_t kMaxRegisterCount = 255;
static const uint32_t kMaxUpvalueCount = 200;
static const uint32_t kMaxLocalCount = 200;
static const uint8_t kInvalidReg = 255;
CompileError::CompileError(const Location& location, const std::string& message)
: location(location)
, message(message)
@ -402,18 +405,37 @@ struct Compiler
}
}
void compileExprFastcallN(AstExprCall* expr, uint8_t target, uint8_t targetCount, bool targetTop, bool multRet, uint8_t regs, int bfid)
void compileExprFastcallN(AstExprCall* expr, uint8_t target, uint8_t targetCount, bool targetTop, bool multRet, uint8_t regs, int bfid, int bfK = -1)
{
LUAU_ASSERT(!expr->self);
LUAU_ASSERT(expr->args.size <= 2);
LUAU_ASSERT(expr->args.size >= 1);
LUAU_ASSERT(expr->args.size <= 2 || (bfid == LBF_BIT32_EXTRACTK && expr->args.size == 3));
LUAU_ASSERT(bfid == LBF_BIT32_EXTRACTK ? bfK >= 0 : bfK < 0);
LuauOpcode opc = expr->args.size == 1 ? LOP_FASTCALL1 : LOP_FASTCALL2;
uint32_t args[2] = {};
if (FFlag::LuauCompileExtractK)
{
opc = expr->args.size == 1 ? LOP_FASTCALL1 : (bfK >= 0 || isConstant(expr->args.data[1])) ? LOP_FASTCALL2K : LOP_FASTCALL2;
}
uint32_t args[3] = {};
for (size_t i = 0; i < expr->args.size; ++i)
{
if (i > 0)
if (FFlag::LuauCompileExtractK)
{
if (i > 0 && opc == LOP_FASTCALL2K)
{
int32_t cid = getConstantIndex(expr->args.data[i]);
if (cid < 0)
CompileError::raise(expr->location, "Exceeded constant limit; simplify the code to compile");
args[i] = cid;
continue; // TODO: remove this and change if below to else if
}
}
else if (i > 0)
{
if (int32_t cid = getConstantIndex(expr->args.data[i]); cid >= 0)
{
@ -424,7 +446,9 @@ struct Compiler
}
if (int reg = getExprLocalReg(expr->args.data[i]); reg >= 0)
{
args[i] = uint8_t(reg);
}
else
{
args[i] = uint8_t(regs + 1 + i);
@ -436,21 +460,31 @@ struct Compiler
bytecode.emitABC(opc, uint8_t(bfid), uint8_t(args[0]), 0);
if (opc != LOP_FASTCALL1)
bytecode.emitAux(args[1]);
bytecode.emitAux(bfK >= 0 ? bfK : args[1]);
// Set up a traditional Lua stack for the subsequent LOP_CALL.
// Note, as with other instructions that immediately follow FASTCALL, these are normally not executed and are used as a fallback for
// these FASTCALL variants.
for (size_t i = 0; i < expr->args.size; ++i)
{
if (i > 0 && opc == LOP_FASTCALL2K)
if (FFlag::LuauCompileExtractK)
{
emitLoadK(uint8_t(regs + 1 + i), args[i]);
break;
if (i > 0 && opc == LOP_FASTCALL2K)
emitLoadK(uint8_t(regs + 1 + i), args[i]);
else if (args[i] != regs + 1 + i)
bytecode.emitABC(LOP_MOVE, uint8_t(regs + 1 + i), uint8_t(args[i]), 0);
}
else
{
if (i > 0 && opc == LOP_FASTCALL2K)
{
emitLoadK(uint8_t(regs + 1 + i), args[i]);
break;
}
if (args[i] != regs + 1 + i)
bytecode.emitABC(LOP_MOVE, uint8_t(regs + 1 + i), uint8_t(args[i]), 0);
if (args[i] != regs + 1 + i)
bytecode.emitABC(LOP_MOVE, uint8_t(regs + 1 + i), uint8_t(args[i]), 0);
}
}
// note, these instructions are normally not executed and are used as a fallback for FASTCALL
@ -599,7 +633,7 @@ struct Compiler
}
else
{
AstExprLocal* le = FFlag::LuauCompileFreeReassign ? getExprLocal(arg) : arg->as<AstExprLocal>();
AstExprLocal* le = getExprLocal(arg);
Variable* lv = le ? variables.find(le->local) : nullptr;
// if the argument is a local that isn't mutated, we will simply reuse the existing register
@ -722,6 +756,26 @@ struct Compiler
bfid = -1;
}
// Optimization: for bit32.extract with constant in-range f/w we compile using FASTCALL2K and a special builtin
if (FFlag::LuauCompileExtractK && bfid == LBF_BIT32_EXTRACT && expr->args.size == 3 && isConstant(expr->args.data[1]) && isConstant(expr->args.data[2]))
{
Constant fc = getConstant(expr->args.data[1]);
Constant wc = getConstant(expr->args.data[2]);
int fi = fc.type == Constant::Type_Number ? int(fc.valueNumber) : -1;
int wi = wc.type == Constant::Type_Number ? int(wc.valueNumber) : -1;
if (fi >= 0 && wi > 0 && fi + wi <= 32)
{
int fwp = fi | ((wi - 1) << 5);
int32_t cid = bytecode.addConstantNumber(fwp);
if (cid < 0)
CompileError::raise(expr->location, "Exceeded constant limit; simplify the code to compile");
return compileExprFastcallN(expr, target, targetCount, targetTop, multRet, regs, LBF_BIT32_EXTRACTK, cid);
}
}
// Optimization: for 1/2 argument fast calls use specialized opcodes
if (bfid >= 0 && expr->args.size >= 1 && expr->args.size <= 2 && !isExprMultRet(expr->args.data[expr->args.size - 1]))
return compileExprFastcallN(expr, target, targetCount, targetTop, multRet, regs, bfid);
@ -1217,7 +1271,7 @@ struct Compiler
{
// disambiguation: there's 4 cases (we only need truthy or falsy results based on onlyTruth)
// onlyTruth = 1: a and b transforms to a ? b : dontcare
// onlyTruth = 1: a or b transforms to a ? a : a
// onlyTruth = 1: a or b transforms to a ? a : b
// onlyTruth = 0: a and b transforms to !a ? a : b
// onlyTruth = 0: a or b transforms to !a ? b : dontcare
if (onlyTruth == (expr->op == AstExprBinary::And))
@ -2107,9 +2161,35 @@ struct Compiler
return reg;
}
// initializes target..target+targetCount-1 range using expression
// if expression is a call/vararg, we assume it returns all values, otherwise we fill the rest with nil
// assumes target register range can be clobbered and is at the top of the register space if targetTop = true
void compileExprTempN(AstExpr* node, uint8_t target, uint8_t targetCount, bool targetTop)
{
// we assume that target range is at the top of the register space and can be clobbered
// this is what allows us to compile the last call expression - if it's a call - using targetTop=true
LUAU_ASSERT(!targetTop || unsigned(target + targetCount) == regTop);
if (AstExprCall* expr = node->as<AstExprCall>())
{
compileExprCall(expr, target, targetCount, targetTop);
}
else if (AstExprVarargs* expr = node->as<AstExprVarargs>())
{
compileExprVarargs(expr, target, targetCount);
}
else
{
compileExprTemp(node, target);
for (size_t i = 1; i < targetCount; ++i)
bytecode.emitABC(LOP_LOADNIL, uint8_t(target + i), 0, 0);
}
}
// initializes target..target+targetCount-1 range using expressions from the list
// if list has fewer expressions, and last expression is a call, we assume the call returns the rest of the values
// if list has fewer expressions, and last expression isn't a call, we fill the rest with nil
// if list has fewer expressions, and last expression is multret, we assume it returns the rest of the values
// if list has fewer expressions, and last expression isn't multret, we fill the rest with nil
// assumes target register range can be clobbered and is at the top of the register space if targetTop = true
void compileExprListTemp(const AstArray<AstExpr*>& list, uint8_t target, uint8_t targetCount, bool targetTop)
{
@ -2139,23 +2219,7 @@ struct Compiler
for (size_t i = 0; i < list.size - 1; ++i)
compileExprTemp(list.data[i], uint8_t(target + i));
AstExpr* last = list.data[list.size - 1];
if (AstExprCall* expr = last->as<AstExprCall>())
{
compileExprCall(expr, uint8_t(target + list.size - 1), uint8_t(targetCount - (list.size - 1)), targetTop);
}
else if (AstExprVarargs* expr = last->as<AstExprVarargs>())
{
compileExprVarargs(expr, uint8_t(target + list.size - 1), uint8_t(targetCount - (list.size - 1)));
}
else
{
compileExprTemp(last, uint8_t(target + list.size - 1));
for (size_t i = list.size; i < targetCount; ++i)
bytecode.emitABC(LOP_LOADNIL, uint8_t(target + i), 0, 0);
}
compileExprTempN(list.data[list.size - 1], uint8_t(target + list.size - 1), uint8_t(targetCount - (list.size - 1)), targetTop);
}
else
{
@ -2639,7 +2703,7 @@ struct Compiler
return;
// Optimization: for 1-1 local assignments, we can reuse the register *if* neither local is mutated
if (FFlag::LuauCompileFreeReassign && options.optimizationLevel >= 1 && stat->vars.size == 1 && stat->values.size == 1)
if (options.optimizationLevel >= 1 && stat->vars.size == 1 && stat->values.size == 1)
{
if (AstExprLocal* re = getExprLocal(stat->values.data[0]))
{
@ -2853,7 +2917,6 @@ struct Compiler
LUAU_ASSERT(vars == regs + 3);
LuauOpcode skipOp = LOP_FORGPREP;
LuauOpcode loopOp = LOP_FORGLOOP;
// Optimization: when we iterate via pairs/ipairs, we generate special bytecode that optimizes the traversal using internal iteration index
// These instructions dynamically check if generator is equal to next/inext and bail out
@ -2865,25 +2928,16 @@ struct Compiler
Builtin builtin = getBuiltin(stat->values.data[0]->as<AstExprCall>()->func, globals, variables);
if (builtin.isGlobal("ipairs")) // for .. in ipairs(t)
{
skipOp = LOP_FORGPREP_INEXT;
loopOp = FFlag::LuauCompileNoIpairs ? LOP_FORGLOOP : LOP_FORGLOOP_INEXT;
}
else if (builtin.isGlobal("pairs")) // for .. in pairs(t)
{
skipOp = LOP_FORGPREP_NEXT;
loopOp = LOP_FORGLOOP;
}
}
else if (stat->values.size == 2)
{
Builtin builtin = getBuiltin(stat->values.data[0], globals, variables);
if (builtin.isGlobal("next")) // for .. in next,t
{
skipOp = LOP_FORGPREP_NEXT;
loopOp = LOP_FORGLOOP;
}
}
}
@ -2909,19 +2963,9 @@ struct Compiler
size_t backLabel = bytecode.emitLabel();
bytecode.emitAD(loopOp, regs, 0);
if (FFlag::LuauCompileNoIpairs)
{
// TODO: remove loopOp as it's a constant now
LUAU_ASSERT(loopOp == LOP_FORGLOOP);
// FORGLOOP uses aux to encode variable count and fast path flag for ipairs traversal in the high bit
bytecode.emitAux((skipOp == LOP_FORGPREP_INEXT ? 0x80000000 : 0) | uint32_t(stat->vars.size));
}
// note: FORGLOOP needs variable count encoded in AUX field, other loop instructions assume a fixed variable count
else if (loopOp == LOP_FORGLOOP)
bytecode.emitAux(uint32_t(stat->vars.size));
// FORGLOOP uses aux to encode variable count and fast path flag for ipairs traversal in the high bit
bytecode.emitAD(LOP_FORGLOOP, regs, 0);
bytecode.emitAux((skipOp == LOP_FORGPREP_INEXT ? 0x80000000 : 0) | uint32_t(stat->vars.size));
size_t endLabel = bytecode.emitLabel();
@ -2936,6 +2980,8 @@ struct Compiler
void resolveAssignConflicts(AstStat* stat, std::vector<LValue>& vars)
{
LUAU_ASSERT(!FFlag::LuauCompileOptimalAssignment);
// regsUsed[i] is true if we have assigned the register during earlier assignments
// regsRemap[i] is set to the register where the original (pre-assignment) copy was made
// note: regsRemap is uninitialized intentionally to speed small assignments up; regsRemap[i] is valid iff regsUsed[i]
@ -2988,12 +3034,86 @@ struct Compiler
}
}
struct Assignment
{
LValue lvalue;
uint8_t conflictReg = kInvalidReg;
uint8_t valueReg = kInvalidReg;
};
void resolveAssignConflicts(AstStat* stat, std::vector<Assignment>& vars, const AstArray<AstExpr*>& values)
{
struct Visitor : AstVisitor
{
Compiler* self;
std::bitset<256> conflict;
std::bitset<256> assigned;
Visitor(Compiler* self)
: self(self)
{
}
bool visit(AstExprLocal* node) override
{
int reg = self->getLocalReg(node->local);
if (reg >= 0 && assigned[reg])
conflict[reg] = true;
return true;
}
};
Visitor visitor(this);
// mark any registers that are used *after* assignment as conflicting
for (size_t i = 0; i < vars.size(); ++i)
{
const LValue& li = vars[i].lvalue;
if (i < values.size)
values.data[i]->visit(&visitor);
if (li.kind == LValue::Kind_Local)
visitor.assigned[li.reg] = true;
}
// mark any registers used in trailing expressions as conflicting as well
for (size_t i = vars.size(); i < values.size; ++i)
values.data[i]->visit(&visitor);
// mark any registers used on left hand side that are also assigned anywhere as conflicting
// this is order-independent because we evaluate all right hand side arguments into registers before doing table assignments
for (const Assignment& var : vars)
{
const LValue& li = var.lvalue;
if ((li.kind == LValue::Kind_IndexName || li.kind == LValue::Kind_IndexNumber || li.kind == LValue::Kind_IndexExpr) &&
visitor.assigned[li.reg])
visitor.conflict[li.reg] = true;
if (li.kind == LValue::Kind_IndexExpr && visitor.assigned[li.index])
visitor.conflict[li.index] = true;
}
// for any conflicting var, we need to allocate a temporary register where the assignment is performed, so that we can move the value later
for (Assignment& var : vars)
{
const LValue& li = var.lvalue;
if (li.kind == LValue::Kind_Local && visitor.conflict[li.reg])
var.conflictReg = allocReg(stat, 1);
}
}
void compileStatAssign(AstStatAssign* stat)
{
RegScope rs(this);
// Optimization: one to one assignments don't require complex conflict resolution machinery and allow us to skip temporary registers for
// locals
// Optimization: one to one assignments don't require complex conflict resolution machinery
if (stat->vars.size == 1 && stat->values.size == 1)
{
LValue var = compileLValue(stat->vars.data[0], rs);
@ -3013,28 +3133,110 @@ struct Compiler
return;
}
// compute all l-values: note that this doesn't assign anything yet but it allocates registers and computes complex expressions on the left
// hand side for example, in "a[expr] = foo" expr will get evaluated here
std::vector<LValue> vars(stat->vars.size);
for (size_t i = 0; i < stat->vars.size; ++i)
vars[i] = compileLValue(stat->vars.data[i], rs);
// perform conflict resolution: if any lvalue refers to a local reg that will be reassigned before that, we save the local variable in a
// temporary reg
resolveAssignConflicts(stat, vars);
// compute values into temporaries
uint8_t regs = allocReg(stat, unsigned(stat->vars.size));
compileExprListTemp(stat->values, regs, uint8_t(stat->vars.size), /* targetTop= */ true);
// assign variables that have associated values; note that if we have fewer values than variables, we'll assign nil because
// compileExprListTemp will generate nils
for (size_t i = 0; i < stat->vars.size; ++i)
if (FFlag::LuauCompileOptimalAssignment)
{
setDebugLine(stat->vars.data[i]);
compileAssign(vars[i], uint8_t(regs + i));
// compute all l-values: note that this doesn't assign anything yet but it allocates registers and computes complex expressions on the
// left hand side - for example, in "a[expr] = foo" expr will get evaluated here
std::vector<Assignment> vars(stat->vars.size);
for (size_t i = 0; i < stat->vars.size; ++i)
vars[i].lvalue = compileLValue(stat->vars.data[i], rs);
// perform conflict resolution: if any expression refers to a local that is assigned before evaluating it, we assign to a temporary
// register after this, vars[i].conflictReg is set for locals that need to be assigned in the second pass
resolveAssignConflicts(stat, vars, stat->values);
// compute rhs into (mostly) fresh registers
// note that when the lhs assigment is a local, we evaluate directly into that register
// this is possible because resolveAssignConflicts renamed conflicting locals into temporaries
// after this, vars[i].valueReg is set to a register with the value for *all* vars, but some have already been assigned
for (size_t i = 0; i < stat->vars.size && i < stat->values.size; ++i)
{
AstExpr* value = stat->values.data[i];
if (i + 1 == stat->values.size && stat->vars.size > stat->values.size)
{
// allocate a consecutive range of regs for all remaining vars and compute everything into temps
// note, this also handles trailing nils
uint8_t rest = uint8_t(stat->vars.size - stat->values.size + 1);
uint8_t temp = allocReg(stat, rest);
compileExprTempN(value, temp, rest, /* targetTop= */ true);
for (size_t j = i; j < stat->vars.size; ++j)
vars[j].valueReg = uint8_t(temp + (j - i));
}
else
{
Assignment& var = vars[i];
// if target is a local, use compileExpr directly to target
if (var.lvalue.kind == LValue::Kind_Local)
{
var.valueReg = (var.conflictReg == kInvalidReg) ? var.lvalue.reg : var.conflictReg;
compileExpr(stat->values.data[i], var.valueReg);
}
else
{
var.valueReg = compileExprAuto(stat->values.data[i], rs);
}
}
}
// compute expressions with side effects for lulz
for (size_t i = stat->vars.size; i < stat->values.size; ++i)
{
RegScope rsi(this);
compileExprAuto(stat->values.data[i], rsi);
}
// almost done... let's assign everything left to right, noting that locals were either written-to directly, or will be written-to in a
// separate pass to avoid conflicts
for (const Assignment& var : vars)
{
LUAU_ASSERT(var.valueReg != kInvalidReg);
if (var.lvalue.kind != LValue::Kind_Local)
{
setDebugLine(var.lvalue.location);
compileAssign(var.lvalue, var.valueReg);
}
}
// all regular local writes are done by the prior loops by computing result directly into target, so this just handles conflicts OR
// local copies from temporary registers in multret context, since in that case we have to allocate consecutive temporaries
for (const Assignment& var : vars)
{
if (var.lvalue.kind == LValue::Kind_Local && var.valueReg != var.lvalue.reg)
bytecode.emitABC(LOP_MOVE, var.lvalue.reg, var.valueReg, 0);
}
}
else
{
// compute all l-values: note that this doesn't assign anything yet but it allocates registers and computes complex expressions on the
// left hand side for example, in "a[expr] = foo" expr will get evaluated here
std::vector<LValue> vars(stat->vars.size);
for (size_t i = 0; i < stat->vars.size; ++i)
vars[i] = compileLValue(stat->vars.data[i], rs);
// perform conflict resolution: if any lvalue refers to a local reg that will be reassigned before that, we save the local variable in a
// temporary reg
resolveAssignConflicts(stat, vars);
// compute values into temporaries
uint8_t regs = allocReg(stat, unsigned(stat->vars.size));
compileExprListTemp(stat->values, regs, uint8_t(stat->vars.size), /* targetTop= */ true);
// assign variables that have associated values; note that if we have fewer values than variables, we'll assign nil because
// compileExprListTemp will generate nils
for (size_t i = 0; i < stat->vars.size; ++i)
{
setDebugLine(stat->vars.data[i]);
compileAssign(vars[i], uint8_t(regs + i));
}
}
}

View file

@ -97,8 +97,8 @@ ifeq ($(config),fuzz)
LDFLAGS+=-fsanitize=address,fuzzer
endif
ifneq ($(CALLGRIND),)
CXXFLAGS+=-DCALLGRIND=$(CALLGRIND)
ifeq ($(config),profile)
CXXFLAGS+=-O2 -DNDEBUG -gdwarf-4 -DCALLGRIND=1
endif
# target-specific flags

View file

@ -66,6 +66,7 @@ target_sources(Luau.CodeGen PRIVATE
# Luau.Analysis Sources
target_sources(Luau.Analysis PRIVATE
Analysis/include/Luau/Anyification.h
Analysis/include/Luau/ApplyTypeFunction.h
Analysis/include/Luau/AstJsonEncoder.h
Analysis/include/Luau/AstQuery.h
@ -115,6 +116,7 @@ target_sources(Luau.Analysis PRIVATE
Analysis/include/Luau/Variant.h
Analysis/include/Luau/VisitTypeVar.h
Analysis/src/Anyification.cpp
Analysis/src/ApplyTypeFunction.cpp
Analysis/src/AstJsonEncoder.cpp
Analysis/src/AstQuery.cpp
@ -126,6 +128,7 @@ target_sources(Luau.Analysis PRIVATE
Analysis/src/ConstraintGraphBuilder.cpp
Analysis/src/ConstraintSolver.cpp
Analysis/src/ConstraintSolverLogger.cpp
Analysis/src/EmbeddedBuiltinDefinitions.cpp
Analysis/src/Error.cpp
Analysis/src/Frontend.cpp
Analysis/src/Instantiation.cpp
@ -155,7 +158,6 @@ target_sources(Luau.Analysis PRIVATE
Analysis/src/TypeVar.cpp
Analysis/src/Unifiable.cpp
Analysis/src/Unifier.cpp
Analysis/src/EmbeddedBuiltinDefinitions.cpp
)
# Luau.VM Sources
@ -347,3 +349,11 @@ if(TARGET Luau.Web)
target_sources(Luau.Web PRIVATE
CLI/Web.cpp)
endif()
if(TARGET Luau.Reduce.CLI)
target_sources(Luau.Reduce.CLI PRIVATE
CLI/Reduce.cpp
CLI/FileUtils.cpp
CLI/FileUtils.h
)
endif()

View file

@ -1209,7 +1209,9 @@ void* lua_newuserdatadtor(lua_State* L, size_t sz, void (*dtor)(void*))
{
luaC_checkGC(L);
luaC_checkthreadsleep(L);
Udata* u = luaU_newudata(L, sz + sizeof(dtor), UTAG_IDTOR);
// make sure sz + sizeof(dtor) doesn't overflow; luaU_newdata will reject SIZE_MAX correctly
size_t as = sz < SIZE_MAX - sizeof(dtor) ? sz + sizeof(dtor) : SIZE_MAX;
Udata* u = luaU_newudata(L, as, UTAG_IDTOR);
memcpy(&u->data + sz, &dtor, sizeof(dtor));
setuvalue(L, L->top, u);
api_incr_top(L);

View file

@ -15,6 +15,8 @@
#include <intrin.h>
#endif
LUAU_FASTFLAGVARIABLE(LuauFasterBit32NoWidth, false)
// luauF functions implement FASTCALL instruction that performs a direct execution of some builtin functions from the VM
// The rule of thumb is that FASTCALL functions can not call user code, yield, fail, or reallocate stack.
// If types of the arguments mismatch, luauF_* needs to return -1 and the execution will fall back to the usual call path
@ -600,24 +602,39 @@ static int luauF_btest(lua_State* L, StkId res, TValue* arg0, int nresults, StkI
static int luauF_extract(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams)
{
if (nparams >= 3 && nresults <= 1 && ttisnumber(arg0) && ttisnumber(args) && ttisnumber(args + 1))
if (nparams >= (3 - FFlag::LuauFasterBit32NoWidth) && nresults <= 1 && ttisnumber(arg0) && ttisnumber(args))
{
double a1 = nvalue(arg0);
double a2 = nvalue(args);
double a3 = nvalue(args + 1);
unsigned n;
luai_num2unsigned(n, a1);
int f = int(a2);
int w = int(a3);
if (f >= 0 && w > 0 && f + w <= 32)
if (nparams == 2)
{
uint32_t m = ~(0xfffffffeu << (w - 1));
uint32_t r = (n >> f) & m;
if (unsigned(f) < 32)
{
uint32_t m = 1;
uint32_t r = (n >> f) & m;
setnvalue(res, double(r));
return 1;
setnvalue(res, double(r));
return 1;
}
}
else if (ttisnumber(args + 1))
{
double a3 = nvalue(args + 1);
int w = int(a3);
if (f >= 0 && w > 0 && f + w <= 32)
{
uint32_t m = ~(0xfffffffeu << (w - 1));
uint32_t r = (n >> f) & m;
setnvalue(res, double(r));
return 1;
}
}
}
@ -676,26 +693,41 @@ static int luauF_lshift(lua_State* L, StkId res, TValue* arg0, int nresults, Stk
static int luauF_replace(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams)
{
if (nparams >= 4 && nresults <= 1 && ttisnumber(arg0) && ttisnumber(args) && ttisnumber(args + 1) && ttisnumber(args + 2))
if (nparams >= (4 - FFlag::LuauFasterBit32NoWidth) && nresults <= 1 && ttisnumber(arg0) && ttisnumber(args) && ttisnumber(args + 1))
{
double a1 = nvalue(arg0);
double a2 = nvalue(args);
double a3 = nvalue(args + 1);
double a4 = nvalue(args + 2);
unsigned n, v;
luai_num2unsigned(n, a1);
luai_num2unsigned(v, a2);
int f = int(a3);
int w = int(a4);
if (f >= 0 && w > 0 && f + w <= 32)
if (nparams == 3)
{
uint32_t m = ~(0xfffffffeu << (w - 1));
uint32_t r = (n & ~(m << f)) | ((v & m) << f);
if (unsigned(f) < 32)
{
uint32_t m = 1;
uint32_t r = (n & ~(m << f)) | ((v & m) << f);
setnvalue(res, double(r));
return 1;
setnvalue(res, double(r));
return 1;
}
}
else if (ttisnumber(args + 2))
{
double a4 = nvalue(args + 2);
int w = int(a4);
if (f >= 0 && w > 0 && f + w <= 32)
{
uint32_t m = ~(0xfffffffeu << (w - 1));
uint32_t r = (n & ~(m << f)) | ((v & m) << f);
setnvalue(res, double(r));
return 1;
}
}
}
@ -1138,6 +1170,31 @@ static int luauF_rawlen(lua_State* L, StkId res, TValue* arg0, int nresults, Stk
return -1;
}
static int luauF_extractk(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams)
{
// args is known to contain a number constant with packed in-range f/w
if (nparams >= 2 && nresults <= 1 && ttisnumber(arg0))
{
double a1 = nvalue(arg0);
double a2 = nvalue(args);
unsigned n;
luai_num2unsigned(n, a1);
int fw = int(a2);
int f = fw & 31;
int w1 = fw >> 5;
uint32_t m = ~(0xfffffffeu << w1);
uint32_t r = (n >> f) & m;
setnvalue(res, double(r));
return 1;
}
return -1;
}
luau_FastFunction luauF_table[256] = {
NULL,
luauF_assert,
@ -1211,4 +1268,6 @@ luau_FastFunction luauF_table[256] = {
luauF_select,
luauF_rawlen,
luauF_extractk,
};

View file

@ -175,8 +175,7 @@ void luaF_freeclosure(lua_State* L, Closure* c, lua_Page* page)
const LocVar* luaF_getlocal(const Proto* f, int local_number, int pc)
{
int i;
for (i = 0; i < f->sizelocvars; i++)
for (int i = 0; i < f->sizelocvars; i++)
{
if (pc >= f->locvars[i].startpc && pc < f->locvars[i].endpc)
{ // is variable active?
@ -185,5 +184,15 @@ const LocVar* luaF_getlocal(const Proto* f, int local_number, int pc)
return &f->locvars[i];
}
}
return NULL; // not found
}
const LocVar* luaF_findlocal(const Proto* f, int local_reg, int pc)
{
for (int i = 0; i < f->sizelocvars; i++)
if (local_reg == f->locvars[i].reg && pc >= f->locvars[i].startpc && pc < f->locvars[i].endpc)
return &f->locvars[i];
return NULL; // not found
}

View file

@ -17,3 +17,4 @@ LUAI_FUNC void luaF_freeclosure(lua_State* L, Closure* c, struct lua_Page* page)
LUAI_FUNC void luaF_unlinkupval(UpVal* uv);
LUAI_FUNC void luaF_freeupval(lua_State* L, UpVal* uv, struct lua_Page* page);
LUAI_FUNC const LocVar* luaF_getlocal(const Proto* func, int local_number, int pc);
LUAI_FUNC const LocVar* luaF_findlocal(const Proto* func, int local_reg, int pc);

View file

@ -345,6 +345,9 @@ static void dumpclosure(FILE* f, Closure* cl)
if (cl->isC)
{
if (cl->c.debugname)
fprintf(f, ",\"name\":\"%s\"", cl->c.debugname + 0);
if (cl->nupvalues)
{
fprintf(f, ",\"upvalues\":[");
@ -354,6 +357,9 @@ static void dumpclosure(FILE* f, Closure* cl)
}
else
{
if (cl->l.p->debugname)
fprintf(f, ",\"name\":\"%s\"", getstr(cl->l.p->debugname));
fprintf(f, ",\"proto\":");
dumpref(f, obj2gco(cl->l.p));
if (cl->nupvalues)
@ -403,7 +409,7 @@ static void dumpthread(FILE* f, lua_State* th)
fprintf(f, ",\"source\":\"");
dumpstringdata(f, p->source->data, p->source->len);
fprintf(f, "\",\"line\":%d", p->abslineinfo ? p->abslineinfo[0] : 0);
fprintf(f, "\",\"line\":%d", p->linedefined);
}
if (th->top > th->stack)
@ -411,6 +417,55 @@ static void dumpthread(FILE* f, lua_State* th)
fprintf(f, ",\"stack\":[");
dumprefs(f, th->stack, th->top - th->stack);
fprintf(f, "]");
CallInfo* ci = th->base_ci;
bool first = true;
fprintf(f, ",\"stacknames\":[");
for (StkId v = th->stack; v < th->top; ++v)
{
if (!iscollectable(v))
continue;
while (ci < th->ci && v >= (ci + 1)->func)
ci++;
if (!first)
fputc(',', f);
first = false;
if (v == ci->func)
{
Closure* cl = ci_func(ci);
if (cl->isC)
{
fprintf(f, "\"frame:%s\"", cl->c.debugname ? cl->c.debugname : "[C]");
}
else
{
Proto* p = cl->l.p;
fprintf(f, "\"frame:");
if (p->source)
dumpstringdata(f, p->source->data, p->source->len);
fprintf(f, ":%d:%s\"", p->linedefined, p->debugname ? getstr(p->debugname) : "");
}
}
else if (isLua(ci))
{
Proto* p = ci_func(ci)->l.p;
int pc = pcRel(ci->savedpc, p);
const LocVar* var = luaF_findlocal(p, int(v - ci->base), pc);
if (var && var->varname)
fprintf(f, "\"%s\"", getstr(var->varname));
else
fprintf(f, "null");
}
else
fprintf(f, "null");
}
fprintf(f, "]");
}
fprintf(f, "}");
}

View file

@ -12,9 +12,6 @@ import json
from color import colored, Color
from tabulate import TablePrinter, Alignment
# Based on rotest, specialized for benchmark results
import influxbench
try:
import matplotlib
import matplotlib.pyplot as plt
@ -721,6 +718,7 @@ def run(args, argsubcb):
argumentSubstituionCallback = argsubcb
if arguments.report_metrics or arguments.print_influx_debugging:
import influxbench
influxReporter = influxbench.InfluxReporter(arguments)
else:
influxReporter = None

View file

@ -4,12 +4,7 @@ import platform
import shlex
import socket
import sys
try:
import requests
except:
print("Please install 'requests' using using '{} -m pip install requests' command and try again".format(sys.executable))
exit(-1)
import requests
_hostname = socket.gethostname()

View file

@ -70,6 +70,7 @@ Sandboxing challenges are [covered in the dedicated section](sandbox).
| `bit32` library | ✔️ | |
| `string.gsub` is stricter about using `%` on special characters only | ✔️ | |
| light C functions | 😞 | this changes semantics of fenv on C functions and has complex implications wrt runtime performance |
| NaN keys are supported for tables with `__newindex` | ✔️ | |
Two things that are important to call out here are various new metamethods for tables and yielding in metamethods. In both cases, there are performance implications to supporting this - our implementation is *very* highly tuned for performance, so any changes that affect the core fundamentals of how Lua works have a price. To support yielding in metamethods we'd need to make the core of the VM more involved, since almost every single "interesting" opcode would need to learn how to be resumable - which also complicates future JIT/AOT story. Metamethods in general are important for extensibility, but very challenging to deal with in implementation, so we err on the side of not supporting any new metamethods unless a strong need arises.

View file

@ -326,4 +326,26 @@ Luau uses comments that start from `!` to control certain aspects of analysis, f
--!nostrict
-- Unknown comment directive 'nostrict'; did you mean 'nonstrict'?"
```
## IntegerParsing (27)
Luau parses hexadecimal and binary literals as 64-bit integers before converting them to Luau numbers. As a result, numbers that exceed 2^64 are silently truncated to 2^64, which can result in unexpected program behavior. This warning flags literals that are truncated:
```
-- Hexadecimal number literal exceeded available precision and has been truncated to 2^64
local x = 0x1111111111111111111111111111111111111
```
## ComparisonPrecedence (28)
Because of operator precedence rules, not X == Y parses as (not X) == Y; however, often the intent was to invert the result of the comparison. This warning flags erroneous conditions like that, as well as flagging cases where two comparisons happen in a row without any parentheses:
```
-- not X == Y is equivalent to (not X) == Y; consider using X ~= Y, or wrap one of the expressions in parentheses to silence
if not x == 5 then
end
-- X <= Y <= Z is equivalent to (X <= Y) <= Z; wrap one of the expressions in parentheses to silence
if 1 <= x <= 3 then
end
```

View file

@ -51,8 +51,10 @@ end
To support self-iterating objects, we modify the iteration protocol as follows: instead of simply expanding the result of expression `iter` into three variables (`gen`, `state` and `index`), we check if the first result has an `__iter` metamethod (which can be the case if it's a table, userdata or another composite object (e.g. a record in the future). If it does, the metamethod is called with `gen` as the first argument, and the returned three values replace `gen`/`state`/`index`. This happens *before* the loop:
```lua
if getmetatable(gen) and getmetatable(gen).__iter then
gen, state, index = getmetatable(gen).__iter(gen)
local genmt = rawgetmetatable(gen) -- pseudo code for getmetatable that bypasses __metatable
local iterf = genmt and rawget(genmt, "__iter")
if iterf then
gen, state, index = iterf(gen)
end
```

View file

@ -22,7 +22,7 @@ std::string bytecodeAsArray(const std::vector<uint8_t>& bytecode)
class AssemblyBuilderX64Fixture
{
public:
void check(std::function<void(AssemblyBuilderX64& build)> f, std::vector<uint8_t> result)
void check(std::function<void(AssemblyBuilderX64& build)> f, std::vector<uint8_t> code, std::vector<uint8_t> data = {})
{
AssemblyBuilderX64 build(/* logText= */ false);
@ -30,9 +30,15 @@ public:
build.finalize();
if (build.code != result)
if (build.code != code)
{
printf("Expected: %s\nReceived: %s\n", bytecodeAsArray(result).c_str(), bytecodeAsArray(build.code).c_str());
printf("Expected code: %s\nReceived code: %s\n", bytecodeAsArray(code).c_str(), bytecodeAsArray(build.code).c_str());
CHECK(false);
}
if (build.data != data)
{
printf("Expected data: %s\nReceived data: %s\n", bytecodeAsArray(data).c_str(), bytecodeAsArray(build.data).c_str());
CHECK(false);
}
}
@ -169,6 +175,7 @@ TEST_CASE_FIXTURE(AssemblyBuilderX64Fixture, "BaseUnaryInstructionForms")
SINGLE_COMPARE(div(rcx), 0x48, 0xf7, 0xf1);
SINGLE_COMPARE(idiv(qword[rax]), 0x48, 0xf7, 0x38);
SINGLE_COMPARE(mul(qword[rax + rbx]), 0x48, 0xf7, 0x24, 0x18);
SINGLE_COMPARE(imul(r9), 0x49, 0xf7, 0xe9);
SINGLE_COMPARE(neg(r9), 0x49, 0xf7, 0xd9);
SINGLE_COMPARE(not_(r12), 0x49, 0xf7, 0xd4);
}
@ -191,6 +198,18 @@ TEST_CASE_FIXTURE(AssemblyBuilderX64Fixture, "FormsOfMov")
SINGLE_COMPARE(mov(byte[rsi], al), 0x88, 0x06);
}
TEST_CASE_FIXTURE(AssemblyBuilderX64Fixture, "FormsOfMovExtended")
{
SINGLE_COMPARE(movsx(eax, byte[rcx]), 0x0f, 0xbe, 0x01);
SINGLE_COMPARE(movsx(r12, byte[r10]), 0x4d, 0x0f, 0xbe, 0x22);
SINGLE_COMPARE(movsx(ebx, word[r11]), 0x41, 0x0f, 0xbf, 0x1b);
SINGLE_COMPARE(movsx(rdx, word[rcx]), 0x48, 0x0f, 0xbf, 0x11);
SINGLE_COMPARE(movzx(eax, byte[rcx]), 0x0f, 0xb6, 0x01);
SINGLE_COMPARE(movzx(r12, byte[r10]), 0x4d, 0x0f, 0xb6, 0x22);
SINGLE_COMPARE(movzx(ebx, word[r11]), 0x41, 0x0f, 0xb7, 0x1b);
SINGLE_COMPARE(movzx(rdx, word[rcx]), 0x48, 0x0f, 0xb7, 0x11);
}
TEST_CASE_FIXTURE(AssemblyBuilderX64Fixture, "FormsOfTest")
{
SINGLE_COMPARE(test(al, 8), 0xf6, 0xc0, 0x08);
@ -230,6 +249,19 @@ TEST_CASE_FIXTURE(AssemblyBuilderX64Fixture, "FormsOfAbsoluteJumps")
SINGLE_COMPARE(call(qword[r14 + rdx * 4]), 0x49, 0xff, 0x14, 0x96);
}
TEST_CASE_FIXTURE(AssemblyBuilderX64Fixture, "FormsOfImul")
{
SINGLE_COMPARE(imul(ecx, esi), 0x0f, 0xaf, 0xce);
SINGLE_COMPARE(imul(r12, rax), 0x4c, 0x0f, 0xaf, 0xe0);
SINGLE_COMPARE(imul(r12, qword[rdx + rdi]), 0x4c, 0x0f, 0xaf, 0x24, 0x3a);
SINGLE_COMPARE(imul(ecx, edx, 8), 0x6b, 0xca, 0x08);
SINGLE_COMPARE(imul(ecx, r9d, 0xabcd), 0x41, 0x69, 0xc9, 0xcd, 0xab, 0x00, 0x00);
SINGLE_COMPARE(imul(r8d, eax, -9), 0x44, 0x6b, 0xc0, 0xf7);
SINGLE_COMPARE(imul(rcx, rdx, 17), 0x48, 0x6b, 0xca, 0x11);
SINGLE_COMPARE(imul(rcx, r12, 0xabcd), 0x49, 0x69, 0xcc, 0xcd, 0xab, 0x00, 0x00);
SINGLE_COMPARE(imul(r12, rax, -13), 0x4c, 0x6b, 0xe0, 0xf3);
}
TEST_CASE_FIXTURE(AssemblyBuilderX64Fixture, "ControlFlow")
{
// Jump back
@ -335,6 +367,7 @@ TEST_CASE_FIXTURE(AssemblyBuilderX64Fixture, "AVXUnaryMergeInstructionForms")
// Coverage for other instructions that follow the same pattern
SINGLE_COMPARE(vcomisd(xmm8, xmm10), 0xc4, 0x41, 0xf9, 0x2f, 0xc2);
SINGLE_COMPARE(vucomisd(xmm1, xmm4), 0xc4, 0xe1, 0xf9, 0x2e, 0xcc);
}
TEST_CASE_FIXTURE(AssemblyBuilderX64Fixture, "AVXMoveInstructionForms")
@ -359,6 +392,25 @@ TEST_CASE_FIXTURE(AssemblyBuilderX64Fixture, "AVXMoveInstructionForms")
SINGLE_COMPARE(vmovups(ymm8, ymmword[r9]), 0xc4, 0x41, 0xfc, 0x10, 0x01);
}
TEST_CASE_FIXTURE(AssemblyBuilderX64Fixture, "AVXConversionInstructionForms")
{
SINGLE_COMPARE(vcvttsd2si(ecx, xmm0), 0xc4, 0xe1, 0x7b, 0x2c, 0xc8);
SINGLE_COMPARE(vcvttsd2si(r9d, xmmword[rcx + rdx]), 0xc4, 0x61, 0x7b, 0x2c, 0x0c, 0x11);
SINGLE_COMPARE(vcvttsd2si(rdx, xmm0), 0xc4, 0xe1, 0xfb, 0x2c, 0xd0);
SINGLE_COMPARE(vcvttsd2si(r13, xmmword[rcx + rdx]), 0xc4, 0x61, 0xfb, 0x2c, 0x2c, 0x11);
SINGLE_COMPARE(vcvtsi2sd(xmm5, xmm10, ecx), 0xc4, 0xe1, 0x2b, 0x2a, 0xe9);
SINGLE_COMPARE(vcvtsi2sd(xmm6, xmm11, dword[rcx + rdx]), 0xc4, 0xe1, 0x23, 0x2a, 0x34, 0x11);
SINGLE_COMPARE(vcvtsi2sd(xmm5, xmm10, r13), 0xc4, 0xc1, 0xab, 0x2a, 0xed);
SINGLE_COMPARE(vcvtsi2sd(xmm6, xmm11, qword[rcx + rdx]), 0xc4, 0xe1, 0xa3, 0x2a, 0x34, 0x11);
}
TEST_CASE_FIXTURE(AssemblyBuilderX64Fixture, "AVXTernaryInstructionForms")
{
SINGLE_COMPARE(vroundsd(xmm7, xmm12, xmm3, 9), 0xc4, 0xe3, 0x99, 0x0b, 0xfb, 0x09);
SINGLE_COMPARE(vroundsd(xmm8, xmm13, xmmword[r13 + rdx], 9), 0xc4, 0x43, 0x91, 0x0b, 0x44, 0x15, 0x00, 0x09);
SINGLE_COMPARE(vroundsd(xmm9, xmm14, xmmword[rcx + r10], 1), 0xc4, 0x23, 0x89, 0x0b, 0x0c, 0x11, 0x01);
}
TEST_CASE_FIXTURE(AssemblyBuilderX64Fixture, "MiscInstructions")
{
SINGLE_COMPARE(int3(), 0xcc);
@ -386,6 +438,11 @@ TEST_CASE("LogTest")
build.neg(qword[rbp + r12 * 2]);
build.mov64(r10, 0x1234567812345678ll);
build.vmovapd(xmmword[rax], xmm11);
build.movzx(eax, byte[rcx]);
build.movsx(rsi, word[r12]);
build.imul(rcx, rdx);
build.imul(rcx, rdx, 8);
build.vroundsd(xmm1, xmm2, xmm3, 5);
build.pop(r12);
build.ret();
build.int3();
@ -409,6 +466,11 @@ TEST_CASE("LogTest")
neg qword ptr [rbp+r12*2]
mov r10,1234567812345678h
vmovapd xmmword ptr [rax],xmm11
movzx eax,byte ptr [rcx]
movsx rsi,word ptr [r12]
imul rcx,rdx
imul rcx,rdx,8
vroundsd xmm1,xmm2,xmm3,5
pop r12
ret
int3
@ -426,6 +488,8 @@ TEST_CASE_FIXTURE(AssemblyBuilderX64Fixture, "Constants")
build.vmovss(xmm2, build.f32(1.0f));
build.vmovsd(xmm3, build.f64(1.0));
build.vmovaps(xmm4, build.f32x4(1.0f, 2.0f, 4.0f, 8.0f));
char arr[16] = "hello world!123";
build.vmovupd(xmm5, build.bytes(arr, 16, 8));
build.ret();
},
{
@ -434,7 +498,20 @@ TEST_CASE_FIXTURE(AssemblyBuilderX64Fixture, "Constants")
0xc4, 0xe1, 0xfa, 0x10, 0x15, 0xe1, 0xff, 0xff, 0xff,
0xc4, 0xe1, 0xfb, 0x10, 0x1d, 0xcc, 0xff, 0xff, 0xff,
0xc4, 0xe1, 0xf8, 0x28, 0x25, 0xab, 0xff, 0xff, 0xff,
0xc4, 0xe1, 0xf9, 0x10, 0x2d, 0x92, 0xff, 0xff, 0xff,
0xc3
},
{
'h', 'e', 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd', '!', '1', '2', '3', 0x0,
0x00, 0x00, 0x80, 0x3f,
0x00, 0x00, 0x00, 0x40,
0x00, 0x00, 0x80, 0x40,
0x00, 0x00, 0x00, 0x41,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // padding to align f32x4
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x3f,
0x00, 0x00, 0x00, 0x00, // padding to align f64
0x00, 0x00, 0x80, 0x3f,
0x21, 0x43, 0x65, 0x87, 0x78, 0x56, 0x34, 0x12,
});
// clang-format on
}
@ -444,7 +521,7 @@ TEST_CASE("ConstantStorage")
AssemblyBuilderX64 build(/* logText= */ false);
for (int i = 0; i <= 3000; i++)
build.vaddss(xmm0, xmm0, build.f32(float(i)));
build.vaddss(xmm0, xmm0, build.f32(1.0f));
build.finalize();
@ -452,9 +529,10 @@ TEST_CASE("ConstantStorage")
for (int i = 0; i <= 3000; i++)
{
float v;
memcpy(&v, &build.data[build.data.size() - (i + 1) * sizeof(float)], sizeof(v));
LUAU_ASSERT(v == float(i));
LUAU_ASSERT(build.data[i * 4 + 0] == 0x00);
LUAU_ASSERT(build.data[i * 4 + 1] == 0x00);
LUAU_ASSERT(build.data[i * 4 + 2] == 0x80);
LUAU_ASSERT(build.data[i * 4 + 3] == 0x3f);
}
}

View file

@ -129,6 +129,7 @@ TEST_CASE_FIXTURE(ACFixture, "empty_program")
CHECK(!ac.entryMap.empty());
CHECK(ac.entryMap.count("table"));
CHECK(ac.entryMap.count("math"));
CHECK_EQ(ac.context, AutocompleteContext::Statement);
}
TEST_CASE_FIXTURE(ACFixture, "local_initializer")
@ -138,6 +139,7 @@ TEST_CASE_FIXTURE(ACFixture, "local_initializer")
auto ac = autocomplete('1');
CHECK(ac.entryMap.count("table"));
CHECK(ac.entryMap.count("math"));
CHECK_EQ(ac.context, AutocompleteContext::Expression);
}
TEST_CASE_FIXTURE(ACFixture, "leave_numbers_alone")
@ -146,6 +148,7 @@ TEST_CASE_FIXTURE(ACFixture, "leave_numbers_alone")
auto ac = autocomplete('1');
CHECK(ac.entryMap.empty());
CHECK_EQ(ac.context, AutocompleteContext::Unknown);
}
TEST_CASE_FIXTURE(ACFixture, "user_defined_globals")
@ -157,6 +160,7 @@ TEST_CASE_FIXTURE(ACFixture, "user_defined_globals")
CHECK(ac.entryMap.count("myLocal"));
CHECK(ac.entryMap.count("table"));
CHECK(ac.entryMap.count("math"));
CHECK_EQ(ac.context, AutocompleteContext::Statement);
}
TEST_CASE_FIXTURE(ACFixture, "dont_suggest_local_before_its_definition")
@ -191,6 +195,7 @@ TEST_CASE_FIXTURE(ACFixture, "recursive_function")
auto ac = autocomplete('1');
CHECK(ac.entryMap.count("foo"));
CHECK_EQ(ac.context, AutocompleteContext::Statement);
}
TEST_CASE_FIXTURE(ACFixture, "nested_recursive_function")
@ -293,6 +298,7 @@ TEST_CASE_FIXTURE(ACBuiltinsFixture, "get_member_completions")
CHECK(ac.entryMap.count("find"));
CHECK(ac.entryMap.count("pack"));
CHECK(!ac.entryMap.count("math"));
CHECK_EQ(ac.context, AutocompleteContext::Property);
}
TEST_CASE_FIXTURE(ACFixture, "nested_member_completions")
@ -306,6 +312,7 @@ TEST_CASE_FIXTURE(ACFixture, "nested_member_completions")
CHECK_EQ(2, ac.entryMap.size());
CHECK(ac.entryMap.count("def"));
CHECK(ac.entryMap.count("egh"));
CHECK_EQ(ac.context, AutocompleteContext::Property);
}
TEST_CASE_FIXTURE(ACFixture, "unsealed_table")
@ -319,6 +326,7 @@ TEST_CASE_FIXTURE(ACFixture, "unsealed_table")
auto ac = autocomplete('1');
CHECK_EQ(1, ac.entryMap.size());
CHECK(ac.entryMap.count("prop"));
CHECK_EQ(ac.context, AutocompleteContext::Property);
}
TEST_CASE_FIXTURE(ACFixture, "unsealed_table_2")
@ -333,6 +341,7 @@ TEST_CASE_FIXTURE(ACFixture, "unsealed_table_2")
auto ac = autocomplete('1');
CHECK_EQ(1, ac.entryMap.size());
CHECK(ac.entryMap.count("prop"));
CHECK_EQ(ac.context, AutocompleteContext::Property);
}
TEST_CASE_FIXTURE(ACFixture, "cyclic_table")
@ -346,6 +355,7 @@ TEST_CASE_FIXTURE(ACFixture, "cyclic_table")
auto ac = autocomplete('1');
CHECK(ac.entryMap.count("abc"));
CHECK_EQ(ac.context, AutocompleteContext::Property);
}
TEST_CASE_FIXTURE(ACFixture, "table_union")
@ -361,6 +371,7 @@ TEST_CASE_FIXTURE(ACFixture, "table_union")
auto ac = autocomplete('1');
CHECK_EQ(1, ac.entryMap.size());
CHECK(ac.entryMap.count("b2"));
CHECK_EQ(ac.context, AutocompleteContext::Property);
}
TEST_CASE_FIXTURE(ACFixture, "table_intersection")
@ -378,6 +389,7 @@ TEST_CASE_FIXTURE(ACFixture, "table_intersection")
CHECK(ac.entryMap.count("a1"));
CHECK(ac.entryMap.count("b2"));
CHECK(ac.entryMap.count("c3"));
CHECK_EQ(ac.context, AutocompleteContext::Property);
}
TEST_CASE_FIXTURE(ACBuiltinsFixture, "get_string_completions")
@ -389,6 +401,7 @@ TEST_CASE_FIXTURE(ACBuiltinsFixture, "get_string_completions")
auto ac = autocomplete('1');
CHECK_EQ(17, ac.entryMap.size());
CHECK_EQ(ac.context, AutocompleteContext::Property);
}
TEST_CASE_FIXTURE(ACFixture, "get_suggestions_for_new_statement")
@ -400,6 +413,7 @@ TEST_CASE_FIXTURE(ACFixture, "get_suggestions_for_new_statement")
CHECK_NE(0, ac.entryMap.size());
CHECK(ac.entryMap.count("table"));
CHECK_EQ(ac.context, AutocompleteContext::Statement);
}
TEST_CASE_FIXTURE(ACFixture, "get_suggestions_for_the_very_start_of_the_script")
@ -412,6 +426,7 @@ TEST_CASE_FIXTURE(ACFixture, "get_suggestions_for_the_very_start_of_the_script")
auto ac = autocomplete('1');
CHECK(ac.entryMap.count("table"));
CHECK_EQ(ac.context, AutocompleteContext::Statement);
}
TEST_CASE_FIXTURE(ACFixture, "method_call_inside_function_body")
@ -429,6 +444,7 @@ TEST_CASE_FIXTURE(ACFixture, "method_call_inside_function_body")
CHECK_NE(0, ac.entryMap.size());
CHECK(!ac.entryMap.count("math"));
CHECK_EQ(ac.context, AutocompleteContext::Property);
}
TEST_CASE_FIXTURE(ACBuiltinsFixture, "method_call_inside_if_conditional")
@ -442,6 +458,7 @@ TEST_CASE_FIXTURE(ACBuiltinsFixture, "method_call_inside_if_conditional")
CHECK_NE(0, ac.entryMap.size());
CHECK(ac.entryMap.count("concat"));
CHECK(!ac.entryMap.count("math"));
CHECK_EQ(ac.context, AutocompleteContext::Property);
}
TEST_CASE_FIXTURE(ACFixture, "statement_between_two_statements")
@ -459,6 +476,8 @@ TEST_CASE_FIXTURE(ACFixture, "statement_between_two_statements")
CHECK_NE(0, ac.entryMap.size());
CHECK(ac.entryMap.count("getmyscripts"));
CHECK_EQ(ac.context, AutocompleteContext::Statement);
}
TEST_CASE_FIXTURE(ACFixture, "bias_toward_inner_scope")
@ -476,6 +495,7 @@ TEST_CASE_FIXTURE(ACFixture, "bias_toward_inner_scope")
auto ac = autocomplete('1');
CHECK(ac.entryMap.count("A"));
CHECK_EQ(ac.context, AutocompleteContext::Statement);
TypeId t = follow(*ac.entryMap["A"].type);
const TableTypeVar* tt = get<TableTypeVar>(t);
@ -489,10 +509,12 @@ TEST_CASE_FIXTURE(ACFixture, "recommend_statement_starting_keywords")
check("@1");
auto ac = autocomplete('1');
CHECK(ac.entryMap.count("local"));
CHECK_EQ(ac.context, AutocompleteContext::Statement);
check("local i = @1");
auto ac2 = autocomplete('1');
CHECK(!ac2.entryMap.count("local"));
CHECK_EQ(ac2.context, AutocompleteContext::Expression);
}
TEST_CASE_FIXTURE(ACFixture, "do_not_overwrite_context_sensitive_kws")
@ -508,6 +530,7 @@ TEST_CASE_FIXTURE(ACFixture, "do_not_overwrite_context_sensitive_kws")
AutocompleteEntry entry = ac.entryMap["continue"];
CHECK(entry.kind == AutocompleteEntryKind::Binding);
CHECK_EQ(ac.context, AutocompleteContext::Statement);
}
TEST_CASE_FIXTURE(ACFixture, "dont_offer_any_suggestions_from_within_a_comment")
@ -525,6 +548,7 @@ TEST_CASE_FIXTURE(ACFixture, "dont_offer_any_suggestions_from_within_a_comment")
auto ac = autocomplete('1');
CHECK_EQ(0, ac.entryMap.size());
CHECK_EQ(ac.context, AutocompleteContext::Unknown);
}
TEST_CASE_FIXTURE(ACFixture, "dont_offer_any_suggestions_from_the_end_of_a_comment")
@ -536,6 +560,7 @@ TEST_CASE_FIXTURE(ACFixture, "dont_offer_any_suggestions_from_the_end_of_a_comme
auto ac = autocomplete('1');
CHECK_EQ(0, ac.entryMap.size());
CHECK_EQ(ac.context, AutocompleteContext::Unknown);
}
TEST_CASE_FIXTURE(ACFixture, "dont_offer_any_suggestions_from_within_a_broken_comment")
@ -547,6 +572,7 @@ TEST_CASE_FIXTURE(ACFixture, "dont_offer_any_suggestions_from_within_a_broken_co
auto ac = autocomplete('1');
CHECK_EQ(0, ac.entryMap.size());
CHECK_EQ(ac.context, AutocompleteContext::Unknown);
}
TEST_CASE_FIXTURE(ACFixture, "dont_offer_any_suggestions_from_within_a_broken_comment_at_the_very_end_of_the_file")
@ -555,6 +581,7 @@ TEST_CASE_FIXTURE(ACFixture, "dont_offer_any_suggestions_from_within_a_broken_co
auto ac = autocomplete('1');
CHECK_EQ(0, ac.entryMap.size());
CHECK_EQ(ac.context, AutocompleteContext::Unknown);
}
TEST_CASE_FIXTURE(ACFixture, "autocomplete_for_middle_keywords")
@ -566,6 +593,7 @@ TEST_CASE_FIXTURE(ACFixture, "autocomplete_for_middle_keywords")
auto ac1 = autocomplete('1');
CHECK_EQ(ac1.entryMap.count("do"), 0);
CHECK_EQ(ac1.entryMap.count("end"), 0);
CHECK_EQ(ac1.context, AutocompleteContext::Unknown);
check(R"(
for x =@1 1
@ -574,6 +602,7 @@ TEST_CASE_FIXTURE(ACFixture, "autocomplete_for_middle_keywords")
auto ac2 = autocomplete('1');
CHECK_EQ(ac2.entryMap.count("do"), 0);
CHECK_EQ(ac2.entryMap.count("end"), 0);
CHECK_EQ(ac2.context, AutocompleteContext::Unknown);
check(R"(
for x = 1,@1 2
@ -582,6 +611,7 @@ TEST_CASE_FIXTURE(ACFixture, "autocomplete_for_middle_keywords")
auto ac3 = autocomplete('1');
CHECK_EQ(1, ac3.entryMap.size());
CHECK_EQ(ac3.entryMap.count("do"), 1);
CHECK_EQ(ac3.context, AutocompleteContext::Keyword);
check(R"(
for x = 1, @12,
@ -590,6 +620,7 @@ TEST_CASE_FIXTURE(ACFixture, "autocomplete_for_middle_keywords")
auto ac4 = autocomplete('1');
CHECK_EQ(ac4.entryMap.count("do"), 0);
CHECK_EQ(ac4.entryMap.count("end"), 0);
CHECK_EQ(ac4.context, AutocompleteContext::Expression);
check(R"(
for x = 1, 2, @15
@ -598,6 +629,7 @@ TEST_CASE_FIXTURE(ACFixture, "autocomplete_for_middle_keywords")
auto ac5 = autocomplete('1');
CHECK_EQ(ac5.entryMap.count("do"), 1);
CHECK_EQ(ac5.entryMap.count("end"), 0);
CHECK_EQ(ac5.context, AutocompleteContext::Keyword);
check(R"(
for x = 1, 2, 5 f@1
@ -606,6 +638,7 @@ TEST_CASE_FIXTURE(ACFixture, "autocomplete_for_middle_keywords")
auto ac6 = autocomplete('1');
CHECK_EQ(ac6.entryMap.size(), 1);
CHECK_EQ(ac6.entryMap.count("do"), 1);
CHECK_EQ(ac6.context, AutocompleteContext::Keyword);
check(R"(
for x = 1, 2, 5 do @1
@ -613,6 +646,7 @@ TEST_CASE_FIXTURE(ACFixture, "autocomplete_for_middle_keywords")
auto ac7 = autocomplete('1');
CHECK_EQ(ac7.entryMap.count("end"), 1);
CHECK_EQ(ac7.context, AutocompleteContext::Statement);
}
TEST_CASE_FIXTURE(ACFixture, "autocomplete_for_in_middle_keywords")
@ -623,6 +657,7 @@ TEST_CASE_FIXTURE(ACFixture, "autocomplete_for_in_middle_keywords")
auto ac1 = autocomplete('1');
CHECK_EQ(0, ac1.entryMap.size());
CHECK_EQ(ac1.context, AutocompleteContext::Unknown);
check(R"(
for x@1 @2
@ -630,10 +665,12 @@ TEST_CASE_FIXTURE(ACFixture, "autocomplete_for_in_middle_keywords")
auto ac2 = autocomplete('1');
CHECK_EQ(0, ac2.entryMap.size());
CHECK_EQ(ac2.context, AutocompleteContext::Unknown);
auto ac2a = autocomplete('2');
CHECK_EQ(1, ac2a.entryMap.size());
CHECK_EQ(1, ac2a.entryMap.count("in"));
CHECK_EQ(ac2a.context, AutocompleteContext::Keyword);
check(R"(
for x in y@1
@ -642,6 +679,7 @@ TEST_CASE_FIXTURE(ACFixture, "autocomplete_for_in_middle_keywords")
auto ac3 = autocomplete('1');
CHECK_EQ(ac3.entryMap.count("table"), 1);
CHECK_EQ(ac3.entryMap.count("do"), 0);
CHECK_EQ(ac3.context, AutocompleteContext::Expression);
check(R"(
for x in y @1
@ -650,6 +688,7 @@ TEST_CASE_FIXTURE(ACFixture, "autocomplete_for_in_middle_keywords")
auto ac4 = autocomplete('1');
CHECK_EQ(ac4.entryMap.size(), 1);
CHECK_EQ(ac4.entryMap.count("do"), 1);
CHECK_EQ(ac4.context, AutocompleteContext::Keyword);
check(R"(
for x in f f@1
@ -658,6 +697,7 @@ TEST_CASE_FIXTURE(ACFixture, "autocomplete_for_in_middle_keywords")
auto ac5 = autocomplete('1');
CHECK_EQ(ac5.entryMap.size(), 1);
CHECK_EQ(ac5.entryMap.count("do"), 1);
CHECK_EQ(ac5.context, AutocompleteContext::Keyword);
check(R"(
for x in y do @1
@ -668,6 +708,7 @@ TEST_CASE_FIXTURE(ACFixture, "autocomplete_for_in_middle_keywords")
CHECK_EQ(ac6.entryMap.count("table"), 1);
CHECK_EQ(ac6.entryMap.count("end"), 1);
CHECK_EQ(ac6.entryMap.count("function"), 1);
CHECK_EQ(ac6.context, AutocompleteContext::Statement);
check(R"(
for x in y do e@1
@ -678,6 +719,7 @@ TEST_CASE_FIXTURE(ACFixture, "autocomplete_for_in_middle_keywords")
CHECK_EQ(ac7.entryMap.count("table"), 1);
CHECK_EQ(ac7.entryMap.count("end"), 1);
CHECK_EQ(ac7.entryMap.count("function"), 1);
CHECK_EQ(ac7.context, AutocompleteContext::Statement);
}
TEST_CASE_FIXTURE(ACFixture, "autocomplete_while_middle_keywords")
@ -689,6 +731,7 @@ TEST_CASE_FIXTURE(ACFixture, "autocomplete_while_middle_keywords")
auto ac1 = autocomplete('1');
CHECK_EQ(ac1.entryMap.count("do"), 0);
CHECK_EQ(ac1.entryMap.count("end"), 0);
CHECK_EQ(ac1.context, AutocompleteContext::Expression);
check(R"(
while true @1
@ -697,6 +740,7 @@ TEST_CASE_FIXTURE(ACFixture, "autocomplete_while_middle_keywords")
auto ac2 = autocomplete('1');
CHECK_EQ(1, ac2.entryMap.size());
CHECK_EQ(ac2.entryMap.count("do"), 1);
CHECK_EQ(ac2.context, AutocompleteContext::Keyword);
check(R"(
while true do @1
@ -704,6 +748,7 @@ TEST_CASE_FIXTURE(ACFixture, "autocomplete_while_middle_keywords")
auto ac3 = autocomplete('1');
CHECK_EQ(ac3.entryMap.count("end"), 1);
CHECK_EQ(ac3.context, AutocompleteContext::Statement);
check(R"(
while true d@1
@ -712,6 +757,7 @@ TEST_CASE_FIXTURE(ACFixture, "autocomplete_while_middle_keywords")
auto ac4 = autocomplete('1');
CHECK_EQ(1, ac4.entryMap.size());
CHECK_EQ(ac4.entryMap.count("do"), 1);
CHECK_EQ(ac4.context, AutocompleteContext::Keyword);
}
TEST_CASE_FIXTURE(ACFixture, "autocomplete_if_middle_keywords")
@ -728,6 +774,7 @@ TEST_CASE_FIXTURE(ACFixture, "autocomplete_if_middle_keywords")
CHECK_EQ(ac1.entryMap.count("else"), 0);
CHECK_EQ(ac1.entryMap.count("elseif"), 0);
CHECK_EQ(ac1.entryMap.count("end"), 0);
CHECK_EQ(ac1.context, AutocompleteContext::Expression);
check(R"(
if x @1
@ -739,6 +786,7 @@ TEST_CASE_FIXTURE(ACFixture, "autocomplete_if_middle_keywords")
CHECK_EQ(ac2.entryMap.count("else"), 0);
CHECK_EQ(ac2.entryMap.count("elseif"), 0);
CHECK_EQ(ac2.entryMap.count("end"), 0);
CHECK_EQ(ac2.context, AutocompleteContext::Keyword);
check(R"(
if x t@1
@ -747,6 +795,7 @@ TEST_CASE_FIXTURE(ACFixture, "autocomplete_if_middle_keywords")
auto ac3 = autocomplete('1');
CHECK_EQ(1, ac3.entryMap.size());
CHECK_EQ(ac3.entryMap.count("then"), 1);
CHECK_EQ(ac3.context, AutocompleteContext::Keyword);
check(R"(
if x then
@ -760,6 +809,7 @@ TEST_CASE_FIXTURE(ACFixture, "autocomplete_if_middle_keywords")
CHECK_EQ(ac4.entryMap.count("function"), 1);
CHECK_EQ(ac4.entryMap.count("elseif"), 1);
CHECK_EQ(ac4.entryMap.count("end"), 0);
CHECK_EQ(ac4.context, AutocompleteContext::Statement);
check(R"(
if x then
@ -772,6 +822,7 @@ TEST_CASE_FIXTURE(ACFixture, "autocomplete_if_middle_keywords")
CHECK_EQ(ac4a.entryMap.count("table"), 1);
CHECK_EQ(ac4a.entryMap.count("else"), 1);
CHECK_EQ(ac4a.entryMap.count("elseif"), 1);
CHECK_EQ(ac4a.context, AutocompleteContext::Statement);
check(R"(
if x then
@ -786,6 +837,7 @@ TEST_CASE_FIXTURE(ACFixture, "autocomplete_if_middle_keywords")
CHECK_EQ(ac5.entryMap.count("else"), 0);
CHECK_EQ(ac5.entryMap.count("elseif"), 0);
CHECK_EQ(ac5.entryMap.count("end"), 0);
CHECK_EQ(ac5.context, AutocompleteContext::Statement);
}
TEST_CASE_FIXTURE(ACFixture, "autocomplete_until_in_repeat")
@ -797,6 +849,7 @@ TEST_CASE_FIXTURE(ACFixture, "autocomplete_until_in_repeat")
auto ac = autocomplete('1');
CHECK_EQ(ac.entryMap.count("table"), 1);
CHECK_EQ(ac.entryMap.count("until"), 1);
CHECK_EQ(ac.context, AutocompleteContext::Statement);
}
TEST_CASE_FIXTURE(ACFixture, "autocomplete_until_expression")
@ -808,6 +861,7 @@ TEST_CASE_FIXTURE(ACFixture, "autocomplete_until_expression")
auto ac = autocomplete('1');
CHECK_EQ(ac.entryMap.count("table"), 1);
CHECK_EQ(ac.context, AutocompleteContext::Expression);
}
TEST_CASE_FIXTURE(ACFixture, "local_names")
@ -819,6 +873,7 @@ TEST_CASE_FIXTURE(ACFixture, "local_names")
auto ac1 = autocomplete('1');
CHECK_EQ(ac1.entryMap.size(), 1);
CHECK_EQ(ac1.entryMap.count("function"), 1);
CHECK_EQ(ac1.context, AutocompleteContext::Unknown);
check(R"(
local ab, cd@1
@ -826,6 +881,7 @@ TEST_CASE_FIXTURE(ACFixture, "local_names")
auto ac2 = autocomplete('1');
CHECK(ac2.entryMap.empty());
CHECK_EQ(ac2.context, AutocompleteContext::Unknown);
}
TEST_CASE_FIXTURE(ACFixture, "autocomplete_end_with_fn_exprs")
@ -836,6 +892,7 @@ TEST_CASE_FIXTURE(ACFixture, "autocomplete_end_with_fn_exprs")
auto ac = autocomplete('1');
CHECK_EQ(ac.entryMap.count("end"), 1);
CHECK_EQ(ac.context, AutocompleteContext::Statement);
}
TEST_CASE_FIXTURE(ACFixture, "autocomplete_end_with_lambda")
@ -846,6 +903,7 @@ TEST_CASE_FIXTURE(ACFixture, "autocomplete_end_with_lambda")
auto ac = autocomplete('1');
CHECK_EQ(ac.entryMap.count("end"), 1);
CHECK_EQ(ac.context, AutocompleteContext::Statement);
}
TEST_CASE_FIXTURE(ACFixture, "stop_at_first_stat_when_recommending_keywords")
@ -858,6 +916,7 @@ TEST_CASE_FIXTURE(ACFixture, "stop_at_first_stat_when_recommending_keywords")
auto ac1 = autocomplete('1');
CHECK_EQ(ac1.entryMap.count("in"), 1);
CHECK_EQ(ac1.entryMap.count("until"), 0);
CHECK_EQ(ac1.context, AutocompleteContext::Keyword);
}
TEST_CASE_FIXTURE(ACFixture, "autocomplete_repeat_middle_keyword")
@ -980,6 +1039,7 @@ TEST_CASE_FIXTURE(ACFixture, "local_function_params")
auto ac2 = autocomplete('1');
CHECK_EQ(ac2.entryMap.count("abc"), 1);
CHECK_EQ(ac2.entryMap.count("def"), 1);
CHECK_EQ(ac2.context, AutocompleteContext::Statement);
check(R"(
local function abc(def, ghi@1)
@ -988,6 +1048,7 @@ TEST_CASE_FIXTURE(ACFixture, "local_function_params")
auto ac3 = autocomplete('1');
CHECK(ac3.entryMap.empty());
CHECK_EQ(ac3.context, AutocompleteContext::Unknown);
}
TEST_CASE_FIXTURE(ACFixture, "global_function_params")
@ -1022,6 +1083,7 @@ TEST_CASE_FIXTURE(ACFixture, "global_function_params")
auto ac2 = autocomplete('1');
CHECK_EQ(ac2.entryMap.count("abc"), 1);
CHECK_EQ(ac2.entryMap.count("def"), 1);
CHECK_EQ(ac2.context, AutocompleteContext::Statement);
check(R"(
function abc(def, ghi@1)
@ -1030,6 +1092,7 @@ TEST_CASE_FIXTURE(ACFixture, "global_function_params")
auto ac3 = autocomplete('1');
CHECK(ac3.entryMap.empty());
CHECK_EQ(ac3.context, AutocompleteContext::Unknown);
}
TEST_CASE_FIXTURE(ACFixture, "arguments_to_global_lambda")
@ -1074,6 +1137,7 @@ TEST_CASE_FIXTURE(ACFixture, "function_expr_params")
auto ac2 = autocomplete('1');
CHECK_EQ(ac2.entryMap.count("def"), 1);
CHECK_EQ(ac2.context, AutocompleteContext::Statement);
}
TEST_CASE_FIXTURE(ACFixture, "local_initializer")
@ -1135,6 +1199,7 @@ local b: string = "don't trip"
CHECK(ac.entryMap.count("nil"));
CHECK(ac.entryMap.count("number"));
CHECK_EQ(ac.context, AutocompleteContext::Type);
}
TEST_CASE_FIXTURE(ACFixture, "private_types")
@ -1203,6 +1268,7 @@ local a: aa
auto ac = Luau::autocomplete(frontend, "Module/B", Position{2, 11}, nullCallback);
CHECK(ac.entryMap.count("aaa"));
CHECK_EQ(ac.context, AutocompleteContext::Type);
}
TEST_CASE_FIXTURE(ACFixture, "module_type_members")
@ -1227,6 +1293,7 @@ local a: aaa.
CHECK_EQ(2, ac.entryMap.size());
CHECK(ac.entryMap.count("A"));
CHECK(ac.entryMap.count("B"));
CHECK_EQ(ac.context, AutocompleteContext::Type);
}
TEST_CASE_FIXTURE(ACFixture, "argument_types")
@ -1240,6 +1307,7 @@ local b: string = "don't trip"
CHECK(ac.entryMap.count("nil"));
CHECK(ac.entryMap.count("number"));
CHECK_EQ(ac.context, AutocompleteContext::Type);
}
TEST_CASE_FIXTURE(ACFixture, "return_types")
@ -1253,6 +1321,7 @@ local b: string = "don't trip"
CHECK(ac.entryMap.count("nil"));
CHECK(ac.entryMap.count("number"));
CHECK_EQ(ac.context, AutocompleteContext::Type);
}
TEST_CASE_FIXTURE(ACFixture, "as_types")
@ -1266,6 +1335,7 @@ local b: number = (a :: n@1
CHECK(ac.entryMap.count("nil"));
CHECK(ac.entryMap.count("number"));
CHECK_EQ(ac.context, AutocompleteContext::Type);
}
TEST_CASE_FIXTURE(ACFixture, "function_type_types")
@ -1314,6 +1384,7 @@ local b: string = "don't trip"
auto ac = autocomplete('1');
CHECK(ac.entryMap.count("Tee"));
CHECK_EQ(ac.context, AutocompleteContext::Type);
}
TEST_CASE_FIXTURE(ACFixture, "type_correct_suggestion_in_argument")
@ -1402,6 +1473,7 @@ local b: Foo = { a = a.@1
CHECK(ac.entryMap.count("one"));
CHECK(ac.entryMap["one"].typeCorrect == TypeCorrectKind::Correct);
CHECK(ac.entryMap["two"].typeCorrect == TypeCorrectKind::None);
CHECK_EQ(ac.context, AutocompleteContext::Property);
check(R"(
type Foo = { a: number, b: string }
@ -1414,6 +1486,7 @@ local b: Foo = { b = a.@1
CHECK(ac.entryMap.count("two"));
CHECK(ac.entryMap["two"].typeCorrect == TypeCorrectKind::Correct);
CHECK(ac.entryMap["one"].typeCorrect == TypeCorrectKind::None);
CHECK_EQ(ac.context, AutocompleteContext::Property);
}
TEST_CASE_FIXTURE(ACFixture, "type_correct_function_return_types")
@ -2395,6 +2468,7 @@ local t: Test = { f@1 }
auto ac = autocomplete('1');
CHECK(ac.entryMap.count("first"));
CHECK(ac.entryMap.count("second"));
CHECK_EQ(ac.context, AutocompleteContext::Property);
// Intersection
check(R"(
@ -2405,6 +2479,7 @@ local t: Test = { f@1 }
ac = autocomplete('1');
CHECK(ac.entryMap.count("first"));
CHECK(ac.entryMap.count("second"));
CHECK_EQ(ac.context, AutocompleteContext::Property);
// Union
check(R"(
@ -2416,6 +2491,7 @@ local t: Test = { s@1 }
CHECK(ac.entryMap.count("second"));
CHECK(!ac.entryMap.count("first"));
CHECK(!ac.entryMap.count("third"));
CHECK_EQ(ac.context, AutocompleteContext::Property);
// No parenthesis suggestion
check(R"(
@ -2426,6 +2502,7 @@ local t: Test = { f@1 }
ac = autocomplete('1');
CHECK(ac.entryMap.count("first"));
CHECK(ac.entryMap["first"].parens == ParenthesesRecommendation::None);
CHECK_EQ(ac.context, AutocompleteContext::Property);
// When key is changed
check(R"(
@ -2436,6 +2513,7 @@ local t: Test = { f@1 = 2 }
ac = autocomplete('1');
CHECK(ac.entryMap.count("first"));
CHECK(ac.entryMap.count("second"));
CHECK_EQ(ac.context, AutocompleteContext::Property);
// Alternative key syntax
check(R"(
@ -2446,6 +2524,7 @@ local t: Test = { ["f@1"] }
ac = autocomplete('1');
CHECK(ac.entryMap.count("first"));
CHECK(ac.entryMap.count("second"));
CHECK_EQ(ac.context, AutocompleteContext::Property);
// Not an alternative key syntax
check(R"(
@ -2456,6 +2535,7 @@ local t: Test = { "f@1" }
ac = autocomplete('1');
CHECK(!ac.entryMap.count("first"));
CHECK(!ac.entryMap.count("second"));
CHECK_EQ(ac.context, AutocompleteContext::String);
// Skip keys that are already defined
check(R"(
@ -2466,6 +2546,7 @@ local t: Test = { first = 2, s@1 }
ac = autocomplete('1');
CHECK(!ac.entryMap.count("first"));
CHECK(ac.entryMap.count("second"));
CHECK_EQ(ac.context, AutocompleteContext::Property);
// Don't skip active key
check(R"(
@ -2476,6 +2557,7 @@ local t: Test = { first@1 }
ac = autocomplete('1');
CHECK(ac.entryMap.count("first"));
CHECK(ac.entryMap.count("second"));
CHECK_EQ(ac.context, AutocompleteContext::Property);
// Inference after first key
check(R"(
@ -2488,6 +2570,7 @@ local t = {
ac = autocomplete('1');
CHECK(ac.entryMap.count("first"));
CHECK(ac.entryMap.count("second"));
CHECK_EQ(ac.context, AutocompleteContext::Property);
check(R"(
local t = {
@ -2499,6 +2582,7 @@ local t = {
ac = autocomplete('1');
CHECK(ac.entryMap.count("first"));
CHECK(ac.entryMap.count("second"));
CHECK_EQ(ac.context, AutocompleteContext::Property);
}
TEST_CASE_FIXTURE(ACFixture, "autocomplete_documentation_symbols")
@ -2542,6 +2626,7 @@ a = if temp then even elseif true then temp else e@9
CHECK(ac.entryMap.count("then") == 0);
CHECK(ac.entryMap.count("else") == 0);
CHECK(ac.entryMap.count("elseif") == 0);
CHECK_EQ(ac.context, AutocompleteContext::Expression);
ac = autocomplete('2');
CHECK(ac.entryMap.count("temp") == 0);
@ -2549,18 +2634,21 @@ a = if temp then even elseif true then temp else e@9
CHECK(ac.entryMap.count("then"));
CHECK(ac.entryMap.count("else") == 0);
CHECK(ac.entryMap.count("elseif") == 0);
CHECK_EQ(ac.context, AutocompleteContext::Keyword);
ac = autocomplete('3');
CHECK(ac.entryMap.count("even"));
CHECK(ac.entryMap.count("then") == 0);
CHECK(ac.entryMap.count("else") == 0);
CHECK(ac.entryMap.count("elseif") == 0);
CHECK_EQ(ac.context, AutocompleteContext::Expression);
ac = autocomplete('4');
CHECK(ac.entryMap.count("even") == 0);
CHECK(ac.entryMap.count("then") == 0);
CHECK(ac.entryMap.count("else"));
CHECK(ac.entryMap.count("elseif"));
CHECK_EQ(ac.context, AutocompleteContext::Keyword);
ac = autocomplete('5');
CHECK(ac.entryMap.count("temp"));
@ -2568,6 +2656,7 @@ a = if temp then even elseif true then temp else e@9
CHECK(ac.entryMap.count("then") == 0);
CHECK(ac.entryMap.count("else") == 0);
CHECK(ac.entryMap.count("elseif") == 0);
CHECK_EQ(ac.context, AutocompleteContext::Expression);
ac = autocomplete('6');
CHECK(ac.entryMap.count("temp") == 0);
@ -2575,6 +2664,7 @@ a = if temp then even elseif true then temp else e@9
CHECK(ac.entryMap.count("then"));
CHECK(ac.entryMap.count("else") == 0);
CHECK(ac.entryMap.count("elseif") == 0);
CHECK_EQ(ac.context, AutocompleteContext::Keyword);
ac = autocomplete('7');
CHECK(ac.entryMap.count("temp"));
@ -2582,17 +2672,20 @@ a = if temp then even elseif true then temp else e@9
CHECK(ac.entryMap.count("then") == 0);
CHECK(ac.entryMap.count("else") == 0);
CHECK(ac.entryMap.count("elseif") == 0);
CHECK_EQ(ac.context, AutocompleteContext::Expression);
ac = autocomplete('8');
CHECK(ac.entryMap.count("even") == 0);
CHECK(ac.entryMap.count("then") == 0);
CHECK(ac.entryMap.count("else"));
CHECK(ac.entryMap.count("elseif"));
CHECK_EQ(ac.context, AutocompleteContext::Keyword);
ac = autocomplete('9');
CHECK(ac.entryMap.count("then") == 0);
CHECK(ac.entryMap.count("else") == 0);
CHECK(ac.entryMap.count("elseif") == 0);
CHECK_EQ(ac.context, AutocompleteContext::Expression);
}
TEST_CASE_FIXTURE(ACFixture, "autocomplete_if_else_regression")
@ -2626,6 +2719,7 @@ local a: A<(number, s@1>
CHECK(ac.entryMap.count("number"));
CHECK(ac.entryMap.count("string"));
CHECK_EQ(ac.context, AutocompleteContext::Type);
}
TEST_CASE_FIXTURE(ACFixture, "autocomplete_first_function_arg_expected_type")
@ -2686,6 +2780,7 @@ type A<T = @1> = () -> T
CHECK(ac.entryMap.count("number"));
CHECK(ac.entryMap.count("string"));
CHECK_EQ(ac.context, AutocompleteContext::Type);
}
TEST_CASE_FIXTURE(ACFixture, "autocomplete_default_type_pack_parameters")
@ -2698,6 +2793,7 @@ type A<T... = ...@1> = () -> T
CHECK(ac.entryMap.count("number"));
CHECK(ac.entryMap.count("string"));
CHECK_EQ(ac.context, AutocompleteContext::Type);
}
TEST_CASE_FIXTURE(ACBuiltinsFixture, "autocomplete_oop_implicit_self")
@ -2752,16 +2848,19 @@ TEST_CASE_FIXTURE(ACFixture, "autocomplete_string_singletons")
CHECK(ac.entryMap.count("cat"));
CHECK(ac.entryMap.count("dog"));
CHECK_EQ(ac.context, AutocompleteContext::String);
ac = autocomplete('2');
CHECK(ac.entryMap.count("\"cat\""));
CHECK(ac.entryMap.count("\"dog\""));
CHECK_EQ(ac.context, AutocompleteContext::Expression);
ac = autocomplete('3');
CHECK(ac.entryMap.count("cat"));
CHECK(ac.entryMap.count("dog"));
CHECK_EQ(ac.context, AutocompleteContext::String);
check(R"(
type tagged = {tag:"cat", fieldx:number} | {tag:"dog", fieldy:number}
@ -2772,6 +2871,7 @@ TEST_CASE_FIXTURE(ACFixture, "autocomplete_string_singletons")
CHECK(ac.entryMap.count("cat"));
CHECK(ac.entryMap.count("dog"));
CHECK_EQ(ac.context, AutocompleteContext::String);
}
TEST_CASE_FIXTURE(ACFixture, "autocomplete_string_singleton_equality")
@ -2808,6 +2908,7 @@ f(@1)
CHECK(ac.entryMap["true"].typeCorrect == TypeCorrectKind::Correct);
REQUIRE(ac.entryMap.count("false"));
CHECK(ac.entryMap["false"].typeCorrect == TypeCorrectKind::None);
CHECK_EQ(ac.context, AutocompleteContext::Expression);
}
TEST_CASE_FIXTURE(ACFixture, "autocomplete_string_singleton_escape")
@ -3088,4 +3189,27 @@ a.@1
CHECK(ac.entryMap.count("y"));
}
TEST_CASE_FIXTURE(ACFixture, "globals_are_order_independent")
{
ScopedFastFlag sff("LuauAutocompleteFixGlobalOrder", true);
check(R"(
local myLocal = 4
function abc0()
local myInnerLocal = 1
@1
end
function abc1()
local myInnerLocal = 1
end
)");
auto ac = autocomplete('1');
CHECK(ac.entryMap.count("myLocal"));
CHECK(ac.entryMap.count("myInnerLocal"));
CHECK(ac.entryMap.count("abc0"));
CHECK(ac.entryMap.count("abc1"));
}
TEST_SUITE_END();

View file

@ -261,8 +261,6 @@ L1: RETURN R0 0
TEST_CASE("ForBytecode")
{
ScopedFastFlag sff("LuauCompileNoIpairs", true);
// basic for loop: variable directly refers to internal iteration index (R2)
CHECK_EQ("\n" + compileFunction0("for i=1,5 do print(i) end"), R"(
LOADN R2 1
@ -349,8 +347,6 @@ RETURN R0 0
TEST_CASE("ForBytecodeBuiltin")
{
ScopedFastFlag sff("LuauCompileNoIpairs", true);
// we generally recognize builtins like pairs/ipairs and emit special opcodes
CHECK_EQ("\n" + compileFunction0("for k,v in ipairs({}) do end"), R"(
GETIMPORT R0 1
@ -2117,6 +2113,69 @@ TEST_CASE("RecursionParse")
{
CHECK_EQ(std::string(e.what()), "Exceeded allowed recursion depth; simplify your block to make the code compile");
}
try
{
Luau::compileOrThrow(bcb, rep("a(", 1500) + "42" + rep(")", 1500));
CHECK(!"Expected exception");
}
catch (std::exception& e)
{
CHECK_EQ(std::string(e.what()), "Exceeded allowed recursion depth; simplify your expression to make the code compile");
}
try
{
Luau::compileOrThrow(bcb, "return " + rep("{", 1500) + "42" + rep("}", 1500));
CHECK(!"Expected exception");
}
catch (std::exception& e)
{
CHECK_EQ(std::string(e.what()), "Exceeded allowed recursion depth; simplify your expression to make the code compile");
}
try
{
Luau::compileOrThrow(bcb, rep("while true do ", 1500) + "print()" + rep(" end", 1500));
CHECK(!"Expected exception");
}
catch (std::exception& e)
{
CHECK_EQ(std::string(e.what()), "Exceeded allowed recursion depth; simplify your expression to make the code compile");
}
try
{
Luau::compileOrThrow(bcb, rep("for i=1,1 do ", 1500) + "print()" + rep(" end", 1500));
CHECK(!"Expected exception");
}
catch (std::exception& e)
{
CHECK_EQ(std::string(e.what()), "Exceeded allowed recursion depth; simplify your expression to make the code compile");
}
#if 0
// This currently requires too much stack space on MSVC/x64 and crashes with stack overflow at recursion depth 935
try
{
Luau::compileOrThrow(bcb, rep("function a() ", 1500) + "print()" + rep(" end", 1500));
CHECK(!"Expected exception");
}
catch (std::exception& e)
{
CHECK_EQ(std::string(e.what()), "Exceeded allowed recursion depth; simplify your block to make the code compile");
}
try
{
Luau::compileOrThrow(bcb, "return " + rep("function() return ", 1500) + "42" + rep(" end", 1500));
CHECK(!"Expected exception");
}
catch (std::exception& e)
{
CHECK_EQ(std::string(e.what()), "Exceeded allowed recursion depth; simplify your block to make the code compile");
}
#endif
}
TEST_CASE("ArrayIndexLiteral")
@ -2163,8 +2222,6 @@ L1: RETURN R3 -1
TEST_CASE("UpvaluesLoopsBytecode")
{
ScopedFastFlag sff("LuauCompileNoIpairs", true);
CHECK_EQ("\n" + compileFunction(R"(
function test()
for i=1,10 do
@ -2780,46 +2837,44 @@ RETURN R0 0
TEST_CASE("AssignmentConflict")
{
ScopedFastFlag sff("LuauCompileOptimalAssignment", true);
// assignments are left to right
CHECK_EQ("\n" + compileFunction0("local a, b a, b = 1, 2"), R"(
LOADNIL R0
LOADNIL R1
LOADN R2 1
LOADN R3 2
MOVE R0 R2
MOVE R1 R3
LOADN R0 1
LOADN R1 2
RETURN R0 0
)");
// if assignment of a local invalidates a direct register reference in later assignments, the value is evacuated to a temp register
// if assignment of a local invalidates a direct register reference in later assignments, the value is assigned to a temp register first
CHECK_EQ("\n" + compileFunction0("local a a, a[1] = 1, 2"), R"(
LOADNIL R0
MOVE R1 R0
LOADN R2 1
LOADN R3 2
MOVE R0 R2
SETTABLEN R3 R1 1
LOADN R1 1
LOADN R2 2
SETTABLEN R2 R0 1
MOVE R0 R1
RETURN R0 0
)");
// note that this doesn't happen if the local assignment happens last naturally
CHECK_EQ("\n" + compileFunction0("local a a[1], a = 1, 2"), R"(
LOADNIL R0
LOADN R1 1
LOADN R2 2
SETTABLEN R1 R0 1
MOVE R0 R2
LOADN R2 1
LOADN R1 2
SETTABLEN R2 R0 1
MOVE R0 R1
RETURN R0 0
)");
// this will happen if assigned register is used in any table expression, including as an object...
CHECK_EQ("\n" + compileFunction0("local a a, a.foo = 1, 2"), R"(
LOADNIL R0
MOVE R1 R0
LOADN R2 1
LOADN R3 2
MOVE R0 R2
SETTABLEKS R3 R1 K0
LOADN R1 1
LOADN R2 2
SETTABLEKS R2 R0 K0
MOVE R0 R1
RETURN R0 0
)");
@ -2827,22 +2882,20 @@ RETURN R0 0
CHECK_EQ("\n" + compileFunction0("local a a, foo[a] = 1, 2"), R"(
LOADNIL R0
GETIMPORT R1 1
MOVE R2 R0
LOADN R3 1
LOADN R4 2
MOVE R0 R3
SETTABLE R4 R1 R2
LOADN R2 1
LOADN R3 2
SETTABLE R3 R1 R0
MOVE R0 R2
RETURN R0 0
)");
// ... or both ...
CHECK_EQ("\n" + compileFunction0("local a a, a[a] = 1, 2"), R"(
LOADNIL R0
MOVE R1 R0
LOADN R2 1
LOADN R3 2
MOVE R0 R2
SETTABLE R3 R1 R1
LOADN R1 1
LOADN R2 2
SETTABLE R2 R0 R0
MOVE R0 R1
RETURN R0 0
)");
@ -2850,14 +2903,12 @@ RETURN R0 0
CHECK_EQ("\n" + compileFunction0("local a, b a, b, a[b] = 1, 2, 3"), R"(
LOADNIL R0
LOADNIL R1
MOVE R2 R0
MOVE R3 R1
LOADN R4 1
LOADN R5 2
LOADN R6 3
MOVE R0 R4
MOVE R1 R5
SETTABLE R6 R2 R3
LOADN R2 1
LOADN R3 2
LOADN R4 3
SETTABLE R4 R0 R1
MOVE R0 R2
MOVE R1 R3
RETURN R0 0
)");
@ -2867,10 +2918,9 @@ RETURN R0 0
LOADNIL R0
GETIMPORT R1 1
ADDK R2 R0 K2
LOADN R3 1
LOADN R4 2
MOVE R0 R3
SETTABLE R4 R1 R2
LOADN R0 1
LOADN R3 2
SETTABLE R3 R1 R2
RETURN R0 0
)");
}
@ -3849,8 +3899,6 @@ RETURN R0 1
TEST_CASE("SharedClosure")
{
ScopedFastFlag sff("LuauCompileFreeReassign", true);
// closures can be shared even if functions refer to upvalues, as long as upvalues are top-level
CHECK_EQ("\n" + compileFunction(R"(
local val = ...
@ -6063,6 +6111,8 @@ return
math.clamp(-1, 0, 1),
math.sign(77),
math.round(7.6),
bit32.extract(-1, 31),
bit32.replace(100, 1, 0),
(type("fin"))
)",
0, 2),
@ -6114,8 +6164,10 @@ LOADK R43 K2
LOADN R44 0
LOADN R45 1
LOADN R46 8
LOADK R47 K3
RETURN R0 48
LOADN R47 1
LOADN R48 101
LOADK R49 K3
RETURN R0 50
)");
}
@ -6126,7 +6178,8 @@ return
math.abs(),
math.max(1, true),
string.byte("abc", 42),
bit32.rshift(10, 42)
bit32.rshift(10, 42),
bit32.extract(1, 2, "3")
)",
0, 2),
R"(
@ -6147,8 +6200,14 @@ L2: LOADN R4 10
FASTCALL2K 39 R4 K7 L3
LOADK R5 K7
GETIMPORT R3 13
CALL R3 2 -1
L3: RETURN R0 -1
CALL R3 2 1
L3: LOADN R5 1
LOADN R6 2
LOADK R7 K14
FASTCALL 34 L4
GETIMPORT R4 16
CALL R4 3 -1
L4: RETURN R0 -1
)");
}
@ -6205,8 +6264,6 @@ RETURN R0 1
TEST_CASE("LocalReassign")
{
ScopedFastFlag sff("LuauCompileFreeReassign", true);
// locals can be re-assigned and the register gets reused
CHECK_EQ("\n" + compileFunction0(R"(
local function test(a, b)
@ -6294,4 +6351,250 @@ RETURN R2 1
)");
}
TEST_CASE("MultipleAssignments")
{
ScopedFastFlag sff("LuauCompileOptimalAssignment", true);
// order of assignments is left to right
CHECK_EQ("\n" + compileFunction0(R"(
local a, b
a, b = f(1), f(2)
)"),
R"(
LOADNIL R0
LOADNIL R1
GETIMPORT R2 1
LOADN R3 1
CALL R2 1 1
MOVE R0 R2
GETIMPORT R2 1
LOADN R3 2
CALL R2 1 1
MOVE R1 R2
RETURN R0 0
)");
// this includes table assignments
CHECK_EQ("\n" + compileFunction0(R"(
local t
t[1], t[2] = 3, 4
)"),
R"(
LOADNIL R0
LOADNIL R1
LOADN R2 3
LOADN R3 4
SETTABLEN R2 R0 1
SETTABLEN R3 R1 2
RETURN R0 0
)");
// semantically, we evaluate the right hand side first; this allows us to e.g swap elements in a table easily
CHECK_EQ("\n" + compileFunction0(R"(
local t = ...
t[1], t[2] = t[2], t[1]
)"),
R"(
GETVARARGS R0 1
GETTABLEN R1 R0 2
GETTABLEN R2 R0 1
SETTABLEN R1 R0 1
SETTABLEN R2 R0 2
RETURN R0 0
)");
// however, we need to optimize local assignments; to do this well, we need to handle assignment conflicts
// let's first go through a few cases where there are no conflicts:
// when multiple assignments have no conflicts (all local vars are read after being assigned), codegen is the same as a series of single
// assignments
CHECK_EQ("\n" + compileFunction0(R"(
local xm1, x, xp1, xi = ...
xm1,x,xp1,xi = x,xp1,xp1+1,xi-1
)"),
R"(
GETVARARGS R0 4
MOVE R0 R1
MOVE R1 R2
ADDK R2 R2 K0
SUBK R3 R3 K0
RETURN R0 0
)");
// similar example to above from a more complex case
CHECK_EQ("\n" + compileFunction0(R"(
local a, b, c, d, e, f, g, h, t1, t2 = ...
h, g, f, e, d, c, b, a = g, f, e, d + t1, c, b, a, t1 + t2
)"),
R"(
GETVARARGS R0 10
MOVE R7 R6
MOVE R6 R5
MOVE R5 R4
ADD R4 R3 R8
MOVE R3 R2
MOVE R2 R1
MOVE R1 R0
ADD R0 R8 R9
RETURN R0 0
)");
// when locals have a conflict, we assign temporaries instead of locals, and at the end copy the values back
// the basic example of this is a swap/rotate
CHECK_EQ("\n" + compileFunction0(R"(
local a, b = ...
a, b = b, a
)"),
R"(
GETVARARGS R0 2
MOVE R2 R1
MOVE R1 R0
MOVE R0 R2
RETURN R0 0
)");
CHECK_EQ("\n" + compileFunction0(R"(
local a, b, c = ...
a, b, c = c, a, b
)"),
R"(
GETVARARGS R0 3
MOVE R3 R2
MOVE R4 R0
MOVE R2 R1
MOVE R0 R3
MOVE R1 R4
RETURN R0 0
)");
CHECK_EQ("\n" + compileFunction0(R"(
local a, b, c = ...
a, b, c = b, c, a
)"),
R"(
GETVARARGS R0 3
MOVE R3 R1
MOVE R1 R2
MOVE R2 R0
MOVE R0 R3
RETURN R0 0
)");
// multiple assignments with multcall handling - foo() evalutes to temporary registers and they are copied out to target
CHECK_EQ("\n" + compileFunction0(R"(
local a, b, c, d = ...
a, b, c, d = 1, foo()
)"),
R"(
GETVARARGS R0 4
LOADN R0 1
GETIMPORT R4 1
CALL R4 0 3
MOVE R1 R4
MOVE R2 R5
MOVE R3 R6
RETURN R0 0
)");
// note that during this we still need to handle local reassignment, eg when table assignments are performed
CHECK_EQ("\n" + compileFunction0(R"(
local a, b, c, d = ...
a, b[a], c[d], d = 1, foo()
)"),
R"(
GETVARARGS R0 4
LOADN R4 1
GETIMPORT R6 1
CALL R6 0 3
SETTABLE R6 R1 R0
SETTABLE R7 R2 R3
MOVE R0 R4
MOVE R3 R8
RETURN R0 0
)");
// multiple assignments with multcall handling - foo evaluates to a single argument so all remaining locals are assigned to nil
// note that here we don't assign the locals directly, as this case is very rare so we use the similar code path as above
CHECK_EQ("\n" + compileFunction0(R"(
local a, b, c, d = ...
a, b, c, d = 1, foo
)"),
R"(
GETVARARGS R0 4
LOADN R0 1
GETIMPORT R4 1
LOADNIL R5
LOADNIL R6
MOVE R1 R4
MOVE R2 R5
MOVE R3 R6
RETURN R0 0
)");
// note that we also try to use locals as a source of assignment directly when assigning fields; this works using old local value when possible
CHECK_EQ("\n" + compileFunction0(R"(
local a, b = ...
a[1], a[2] = b, b + 1
)"),
R"(
GETVARARGS R0 2
ADDK R2 R1 K0
SETTABLEN R1 R0 1
SETTABLEN R2 R0 2
RETURN R0 0
)");
// ... of course if the local is reassigned, we defer the assignment until later
CHECK_EQ("\n" + compileFunction0(R"(
local a, b = ...
b, a[1] = 42, b
)"),
R"(
GETVARARGS R0 2
LOADN R2 42
SETTABLEN R1 R0 1
MOVE R1 R2
RETURN R0 0
)");
// when there are more expressions when values, we evalute them for side effects, but they also participate in conflict handling
CHECK_EQ("\n" + compileFunction0(R"(
local a, b = ...
a, b = 1, 2, a + b
)"),
R"(
GETVARARGS R0 2
LOADN R2 1
LOADN R3 2
ADD R4 R0 R1
MOVE R0 R2
MOVE R1 R3
RETURN R0 0
)");
}
TEST_CASE("BuiltinExtractK")
{
ScopedFastFlag sff("LuauCompileExtractK", true);
// below, K0 refers to a packed f+w constant for bit32.extractk builtin
// K1 and K2 refer to 1 and 3 and are only used during fallback path
CHECK_EQ("\n" + compileFunction0(R"(
local v = ...
return bit32.extract(v, 1, 3)
)"), R"(
GETVARARGS R0 1
FASTCALL2K 59 R0 K0 L0
MOVE R2 R0
LOADK R3 K1
LOADK R4 K2
GETIMPORT R1 5
CALL R1 3 -1
L0: RETURN R1 -1
)");
}
TEST_SUITE_END();

View file

@ -724,6 +724,23 @@ TEST_CASE("Reference")
CHECK(dtorhits == 2);
}
TEST_CASE("NewUserdataOverflow")
{
StateRef globalState(luaL_newstate(), lua_close);
lua_State* L = globalState.get();
lua_pushcfunction(L, [](lua_State* L1) {
// The following userdata request might cause an overflow.
lua_newuserdatadtor(L1, SIZE_MAX, [](void* d){});
// The overflow might segfault in the following call.
lua_getmetatable(L1, -1);
return 0;
}, nullptr);
CHECK(lua_pcall(L, 0, 0, 0) == LUA_ERRRUN);
CHECK(strcmp(lua_tostring(L, -1), "memory allocation error: block too big") == 0);
}
TEST_CASE("ApiTables")
{
StateRef globalState(luaL_newstate(), lua_close);

View file

@ -372,17 +372,25 @@ void Fixture::registerTestTypes()
void Fixture::dumpErrors(const CheckResult& cr)
{
dumpErrors(std::cout, cr.errors);
std::string error = getErrors(cr);
if (!error.empty())
MESSAGE(error);
}
void Fixture::dumpErrors(const ModulePtr& module)
{
dumpErrors(std::cout, module->errors);
std::stringstream ss;
dumpErrors(ss, module->errors);
if (!ss.str().empty())
MESSAGE(ss.str());
}
void Fixture::dumpErrors(const Module& module)
{
dumpErrors(std::cout, module.errors);
std::stringstream ss;
dumpErrors(ss, module.errors);
if (!ss.str().empty())
MESSAGE(ss.str());
}
std::string Fixture::getErrors(const CheckResult& cr)
@ -413,6 +421,7 @@ LoadDefinitionFileResult Fixture::loadDefinition(const std::string& source)
LoadDefinitionFileResult result = frontend.loadDefinitionFile(source, "@test");
freeze(typeChecker.globalTypes);
dumpErrors(result.module);
REQUIRE_MESSAGE(result.success, "loadDefinition: unable to load definition file");
return result;
}
@ -434,7 +443,8 @@ BuiltinsFixture::BuiltinsFixture(bool freeze, bool prepareAutocomplete)
ConstraintGraphBuilderFixture::ConstraintGraphBuilderFixture()
: Fixture()
, cgb(mainModuleName, &arena, NotNull(&ice), frontend.getGlobalScope())
, mainModule(new Module)
, cgb(mainModuleName, mainModule, &arena, NotNull(&ice), frontend.getGlobalScope())
, forceTheFlag{"DebugLuauDeferredConstraintResolution", true}
{
BlockedTypeVar::nextIndex = 0;

View file

@ -162,6 +162,7 @@ struct BuiltinsFixture : Fixture
struct ConstraintGraphBuilderFixture : Fixture
{
TypeArena arena;
ModulePtr mainModule;
ConstraintGraphBuilder cgb;
ScopedFastFlag forceTheFlag;

View file

@ -1689,7 +1689,7 @@ TEST_CASE_FIXTURE(Fixture, "TestStringInterpolation")
REQUIRE_EQ(result.warnings.size(), 1);
}
TEST_CASE_FIXTURE(Fixture, "LintIntegerParsing")
TEST_CASE_FIXTURE(Fixture, "IntegerParsing")
{
ScopedFastFlag luauLintParseIntegerIssues{"LuauLintParseIntegerIssues", true};
@ -1704,7 +1704,7 @@ local _ = 0x10000000000000000
}
// TODO: remove with FFlagLuauErrorDoubleHexPrefix
TEST_CASE_FIXTURE(Fixture, "LintIntegerParsingDoublePrefix")
TEST_CASE_FIXTURE(Fixture, "IntegerParsingDoublePrefix")
{
ScopedFastFlag luauLintParseIntegerIssues{"LuauLintParseIntegerIssues", true};
ScopedFastFlag luauErrorDoubleHexPrefix{"LuauErrorDoubleHexPrefix", false}; // Lint will be available until we start rejecting code
@ -1721,4 +1721,36 @@ local _ = 0x0xffffffffffffffffffffffffffffffffff
"Hexadecimal number literal has a double prefix, which will fail to parse in the future; remove the extra 0x to fix");
}
TEST_CASE_FIXTURE(Fixture, "ComparisonPrecedence")
{
ScopedFastFlag sff("LuauLintComparisonPrecedence", true);
LintResult result = lint(R"(
local a, b = ...
local _ = not a == b
local _ = not a ~= b
local _ = not a <= b
local _ = a <= b == 0
local _ = not a == not b -- weird but ok
-- silence tests for all of the above
local _ = not (a == b)
local _ = (not a) == b
local _ = not (a ~= b)
local _ = (not a) ~= b
local _ = not (a <= b)
local _ = (not a) <= b
local _ = (a <= b) == 0
local _ = a <= (b == 0)
)");
REQUIRE_EQ(result.warnings.size(), 4);
CHECK_EQ(result.warnings[0].text, "not X == Y is equivalent to (not X) == Y; consider using X ~= Y, or wrap one of the expressions in parentheses to silence");
CHECK_EQ(result.warnings[1].text, "not X ~= Y is equivalent to (not X) ~= Y; consider using X == Y, or wrap one of the expressions in parentheses to silence");
CHECK_EQ(result.warnings[2].text, "not X <= Y is equivalent to (not X) <= Y; wrap one of the expressions in parentheses to silence");
CHECK_EQ(result.warnings[3].text, "X <= Y == Z is equivalent to (X <= Y) == Z; wrap one of the expressions in parentheses to silence");
}
TEST_SUITE_END();

View file

@ -317,4 +317,78 @@ type B = A
CHECK(toString(it->second.type) == "any");
}
TEST_CASE_FIXTURE(BuiltinsFixture, "do_not_clone_reexports")
{
ScopedFastFlag flags[] = {
{"LuauClonePublicInterfaceLess", true},
{"LuauSubstitutionReentrant", true},
{"LuauClassTypeVarsInSubstitution", true},
{"LuauSubstitutionFixMissingFields", true},
};
fileResolver.source["Module/A"] = R"(
export type A = {p : number}
return {}
)";
fileResolver.source["Module/B"] = R"(
local a = require(script.Parent.A)
export type B = {q : a.A}
return {}
)";
CheckResult result = frontend.check("Module/B");
LUAU_REQUIRE_NO_ERRORS(result);
ModulePtr modA = frontend.moduleResolver.getModule("Module/A");
ModulePtr modB = frontend.moduleResolver.getModule("Module/B");
REQUIRE(modA);
REQUIRE(modB);
auto modAiter = modA->getModuleScope()->exportedTypeBindings.find("A");
auto modBiter = modB->getModuleScope()->exportedTypeBindings.find("B");
REQUIRE(modAiter != modA->getModuleScope()->exportedTypeBindings.end());
REQUIRE(modBiter != modB->getModuleScope()->exportedTypeBindings.end());
TypeId typeA = modAiter->second.type;
TypeId typeB = modBiter->second.type;
TableTypeVar* tableB = getMutable<TableTypeVar>(typeB);
REQUIRE(tableB);
CHECK(typeA == tableB->props["q"].type);
}
TEST_CASE_FIXTURE(BuiltinsFixture, "do_not_clone_types_of_reexported_values")
{
ScopedFastFlag flags[] = {
{"LuauClonePublicInterfaceLess", true},
{"LuauSubstitutionReentrant", true},
{"LuauClassTypeVarsInSubstitution", true},
{"LuauSubstitutionFixMissingFields", true},
};
fileResolver.source["Module/A"] = R"(
local exports = {a={p=5}}
return exports
)";
fileResolver.source["Module/B"] = R"(
local a = require(script.Parent.A)
local exports = {b=a.a}
return exports
)";
CheckResult result = frontend.check("Module/B");
LUAU_REQUIRE_NO_ERRORS(result);
ModulePtr modA = frontend.moduleResolver.getModule("Module/A");
ModulePtr modB = frontend.moduleResolver.getModule("Module/B");
REQUIRE(modA);
REQUIRE(modB);
std::optional<TypeId> typeA = first(modA->getModuleScope()->returnType);
std::optional<TypeId> typeB = first(modB->getModuleScope()->returnType);
REQUIRE(typeA);
REQUIRE(typeB);
TableTypeVar* tableA = getMutable<TableTypeVar>(*typeA);
TableTypeVar* tableB = getMutable<TableTypeVar>(*typeB);
CHECK(tableA->props["a"].type == tableB->props["b"].type);
}
TEST_SUITE_END();

View file

@ -169,6 +169,7 @@ TEST_CASE_FIXTURE(Fixture, "table_props_are_any")
REQUIRE(ttv != nullptr);
REQUIRE(ttv->props.count("foo"));
TypeId fooProp = ttv->props["foo"].type;
REQUIRE(fooProp != nullptr);

View file

@ -12,6 +12,11 @@ using namespace Luau;
struct NormalizeFixture : Fixture
{
ScopedFastFlag sff1{"LuauLowerBoundsCalculation", true};
bool isSubtype(TypeId a, TypeId b)
{
return ::Luau::isSubtype(a, b, NotNull{getMainModule()->getModuleScope().get()}, ice);
}
};
void createSomeClasses(TypeChecker& typeChecker)
@ -49,12 +54,6 @@ void createSomeClasses(TypeChecker& typeChecker)
freeze(arena);
}
static bool isSubtype(TypeId a, TypeId b)
{
InternalErrorReporter ice;
return isSubtype(a, b, ice);
}
TEST_SUITE_BEGIN("isSubtype");
TEST_CASE_FIXTURE(NormalizeFixture, "primitives")
@ -511,6 +510,8 @@ TEST_CASE_FIXTURE(NormalizeFixture, "classes")
{
createSomeClasses(typeChecker);
check(""); // Ensure that we have a main Module.
TypeId p = typeChecker.globalScope->lookupType("Parent")->type;
TypeId c = typeChecker.globalScope->lookupType("Child")->type;
TypeId u = typeChecker.globalScope->lookupType("Unrelated")->type;
@ -595,6 +596,7 @@ TEST_CASE_FIXTURE(NormalizeFixture, "union_with_overlapping_field_that_has_a_sub
)");
ModulePtr tempModule{new Module};
tempModule->scopes.emplace_back(Location(), std::make_shared<Scope>(getSingletonTypes().anyTypePack));
// HACK: Normalization is an in-place operation. We need to cheat a little here and unfreeze
// the arena that the type lives in.
@ -880,7 +882,6 @@ TEST_CASE_FIXTURE(Fixture, "intersection_inside_a_table_inside_another_intersect
{
ScopedFastFlag flags[] = {
{"LuauLowerBoundsCalculation", true},
{"LuauQuantifyConstrained", true},
};
// We use a function and inferred parameter types to prevent intermediate normalizations from being performed.
@ -921,7 +922,6 @@ TEST_CASE_FIXTURE(Fixture, "intersection_inside_a_table_inside_another_intersect
{
ScopedFastFlag flags[] = {
{"LuauLowerBoundsCalculation", true},
{"LuauQuantifyConstrained", true},
};
// We use a function and inferred parameter types to prevent intermediate normalizations from being performed.
@ -961,7 +961,6 @@ TEST_CASE_FIXTURE(Fixture, "intersection_inside_a_table_inside_another_intersect
{
ScopedFastFlag flags[] = {
{"LuauLowerBoundsCalculation", true},
{"LuauQuantifyConstrained", true},
};
// We use a function and inferred parameter types to prevent intermediate normalizations from being performed.
@ -1149,7 +1148,6 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "normalization_does_not_convert_ever")
{
ScopedFastFlag sff[]{
{"LuauLowerBoundsCalculation", true},
{"LuauQuantifyConstrained", true},
};
CheckResult result = check(R"(

View file

@ -2658,7 +2658,6 @@ end
TEST_CASE_FIXTURE(Fixture, "error_message_for_using_function_as_type_annotation")
{
ScopedFastFlag sff{"LuauParserFunctionKeywordAsTypeHelp", true};
ParseResult result = tryParse(R"(
type Foo = function
)");

View file

@ -11,6 +11,33 @@ using namespace Luau;
TEST_SUITE_BEGIN("DefinitionTests");
TEST_CASE_FIXTURE(Fixture, "definition_file_simple")
{
loadDefinition(R"(
declare foo: number
declare function bar(x: number): string
declare foo2: typeof(foo)
)");
TypeId globalFooTy = getGlobalBinding(frontend.typeChecker, "foo");
CHECK_EQ(toString(globalFooTy), "number");
TypeId globalBarTy = getGlobalBinding(frontend.typeChecker, "bar");
CHECK_EQ(toString(globalBarTy), "(number) -> string");
TypeId globalFoo2Ty = getGlobalBinding(frontend.typeChecker, "foo2");
CHECK_EQ(toString(globalFoo2Ty), "number");
CheckResult result = check(R"(
local x: number = foo - 1
local y: string = bar(x)
local z: number | string = x
z = y
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "definition_file_loading")
{
loadDefinition(R"(

View file

@ -1637,7 +1637,6 @@ TEST_CASE_FIXTURE(Fixture, "quantify_constrained_types")
{
ScopedFastFlag sff[]{
{"LuauLowerBoundsCalculation", true},
{"LuauQuantifyConstrained", true},
};
CheckResult result = check(R"(
@ -1662,7 +1661,6 @@ TEST_CASE_FIXTURE(Fixture, "call_o_with_another_argument_after_foo_was_quantifie
{
ScopedFastFlag sff[]{
{"LuauLowerBoundsCalculation", true},
{"LuauQuantifyConstrained", true},
};
CheckResult result = check(R"(

View file

@ -845,6 +845,7 @@ TEST_CASE_FIXTURE(Fixture, "generic_table_method")
TableTypeVar* tTable = getMutable<TableTypeVar>(tType);
REQUIRE(tTable != nullptr);
REQUIRE(tTable->props.count("bar"));
TypeId barType = tTable->props["bar"].type;
REQUIRE(barType != nullptr);

View file

@ -329,6 +329,35 @@ function tbl:foo(b: number, c: number)
-- introduce BoundTypeVar to imported type
arrayops.foo(self._regions)
end
-- this alias decreases function type level and causes a demotion of its type
type Table = typeof(tbl)
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(BuiltinsFixture, "do_not_modify_imported_types_5")
{
ScopedFastFlag luauInplaceDemoteSkipAllBound{"LuauInplaceDemoteSkipAllBound", true};
fileResolver.source["game/A"] = R"(
export type Type = {x: number, y: number}
local arrayops = {}
function arrayops.foo(x: Type) end
return arrayops
)";
CheckResult result = check(R"(
local arrayops = require(game.A)
local tbl = {}
tbl.a = 2
function tbl:foo(b: number, c: number)
-- introduce boundTo TableTypeVar to imported type
self.x.a = 2
arrayops.foo(self.x)
end
-- this alias decreases function type level and causes a demotion of its type
type Table = typeof(tbl)
)");
@ -398,8 +427,6 @@ caused by:
TEST_CASE_FIXTURE(BuiltinsFixture, "constrained_anyification_clone_immutable_types")
{
ScopedFastFlag luauAnyificationMustClone{"LuauAnyificationMustClone", true};
fileResolver.source["game/A"] = R"(
return function(...) end
)";

View file

@ -485,7 +485,6 @@ TEST_CASE_FIXTURE(Fixture, "constrained_is_level_dependent")
{
ScopedFastFlag sff[]{
{"LuauLowerBoundsCalculation", true},
{"LuauQuantifyConstrained", true},
};
CheckResult result = check(R"(

View file

@ -41,6 +41,7 @@ struct RefinementClassFixture : Fixture
RefinementClassFixture()
{
TypeArena& arena = typeChecker.globalTypes;
NotNull<Scope> scope{typeChecker.globalScope.get()};
unfreeze(arena);
TypeId vec3 = arena.addType(ClassTypeVar{"Vector3", {}, std::nullopt, std::nullopt, {}, nullptr, "Test"});
@ -49,7 +50,7 @@ struct RefinementClassFixture : Fixture
{"Y", Property{typeChecker.numberType}},
{"Z", Property{typeChecker.numberType}},
};
normalize(vec3, arena, *typeChecker.iceHandler);
normalize(vec3, scope, arena, *typeChecker.iceHandler);
TypeId inst = arena.addType(ClassTypeVar{"Instance", {}, std::nullopt, std::nullopt, {}, nullptr, "Test"});
@ -57,21 +58,21 @@ struct RefinementClassFixture : Fixture
TypePackId isARets = arena.addTypePack({typeChecker.booleanType});
TypeId isA = arena.addType(FunctionTypeVar{isAParams, isARets});
getMutable<FunctionTypeVar>(isA)->magicFunction = magicFunctionInstanceIsA;
normalize(isA, arena, *typeChecker.iceHandler);
normalize(isA, scope, arena, *typeChecker.iceHandler);
getMutable<ClassTypeVar>(inst)->props = {
{"Name", Property{typeChecker.stringType}},
{"IsA", Property{isA}},
};
normalize(inst, arena, *typeChecker.iceHandler);
normalize(inst, scope, arena, *typeChecker.iceHandler);
TypeId folder = typeChecker.globalTypes.addType(ClassTypeVar{"Folder", {}, inst, std::nullopt, {}, nullptr, "Test"});
normalize(folder, arena, *typeChecker.iceHandler);
normalize(folder, scope, arena, *typeChecker.iceHandler);
TypeId part = typeChecker.globalTypes.addType(ClassTypeVar{"Part", {}, inst, std::nullopt, {}, nullptr, "Test"});
getMutable<ClassTypeVar>(part)->props = {
{"Position", Property{vec3}},
};
normalize(part, arena, *typeChecker.iceHandler);
normalize(part, scope, arena, *typeChecker.iceHandler);
typeChecker.globalScope->exportedTypeBindings["Vector3"] = TypeFun{{}, vec3};
typeChecker.globalScope->exportedTypeBindings["Instance"] = TypeFun{{}, inst};
@ -934,8 +935,6 @@ TEST_CASE_FIXTURE(Fixture, "apply_refinements_on_astexprindexexpr_whose_subscrip
TEST_CASE_FIXTURE(Fixture, "discriminate_from_truthiness_of_x")
{
ScopedFastFlag sff{"LuauFalsyPredicateReturnsNilInstead", true};
CheckResult result = check(R"(
type T = {tag: "missing", x: nil} | {tag: "exists", x: string}
@ -1230,8 +1229,6 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "refine_unknowns")
TEST_CASE_FIXTURE(BuiltinsFixture, "falsiness_of_TruthyPredicate_narrows_into_nil")
{
ScopedFastFlag sff{"LuauFalsyPredicateReturnsNilInstead", true};
CheckResult result = check(R"(
local function f(t: {number})
local x = t[1]

View file

@ -1847,6 +1847,7 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "quantifying_a_bound_var_works")
TypeId ty = requireType("clazz");
TableTypeVar* ttv = getMutable<TableTypeVar>(ty);
REQUIRE(ttv);
REQUIRE(ttv->props.count("new"));
Property& prop = ttv->props["new"];
REQUIRE(prop.type);
const FunctionTypeVar* ftv = get<FunctionTypeVar>(follow(prop.type));
@ -2516,6 +2517,7 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "dont_quantify_table_that_belongs_to_outer_sc
TableTypeVar* counterType = getMutable<TableTypeVar>(requireType("Counter"));
REQUIRE(counterType);
REQUIRE(counterType->props.count("new"));
const FunctionTypeVar* newType = get<FunctionTypeVar>(follow(counterType->props["new"].type));
REQUIRE(newType);
@ -3001,8 +3003,6 @@ TEST_CASE_FIXTURE(Fixture, "expected_indexer_from_table_union")
TEST_CASE_FIXTURE(Fixture, "prop_access_on_key_whose_types_mismatches")
{
ScopedFastFlag sff{"LuauReportErrorsOnIndexerKeyMismatch", true};
CheckResult result = check(R"(
local t: {number} = {}
local x = t.x
@ -3014,8 +3014,6 @@ TEST_CASE_FIXTURE(Fixture, "prop_access_on_key_whose_types_mismatches")
TEST_CASE_FIXTURE(Fixture, "prop_access_on_unions_of_indexers_where_key_whose_types_mismatches")
{
ScopedFastFlag sff{"LuauReportErrorsOnIndexerKeyMismatch", true};
CheckResult result = check(R"(
local t: { [number]: number } | { [boolean]: number } = {}
local u = t.x
@ -3081,8 +3079,6 @@ TEST_CASE_FIXTURE(Fixture, "quantify_even_that_table_was_never_exported_at_all")
TEST_CASE_FIXTURE(BuiltinsFixture, "leaking_bad_metatable_errors")
{
ScopedFastFlag luauIndexSilenceErrors{"LuauIndexSilenceErrors", true};
CheckResult result = check(R"(
local a = setmetatable({}, 1)
local b = a.x

View file

@ -17,7 +17,8 @@ struct TryUnifyFixture : Fixture
ScopePtr globalScope{new Scope{arena.addTypePack({TypeId{}})}};
InternalErrorReporter iceHandler;
UnifierSharedState unifierState{&iceHandler};
Unifier state{&arena, Mode::Strict, Location{}, Variance::Covariant, unifierState};
Unifier state{&arena, Mode::Strict, NotNull{globalScope.get()}, Location{}, Variance::Covariant, unifierState};
};
TEST_SUITE_BEGIN("TryUnifyTests");

View file

@ -182,6 +182,22 @@ TEST_CASE_FIXTURE(Fixture, "UnionTypeVarIterator_with_empty_union")
CHECK(actual.empty());
}
TEST_CASE_FIXTURE(Fixture, "UnionTypeVarIterator_with_only_cyclic_union")
{
TypeVar tv{UnionTypeVar{}};
auto utv = getMutable<UnionTypeVar>(&tv);
utv->options.push_back(&tv);
utv->options.push_back(&tv);
std::vector<TypeId> actual(begin(utv), end(utv));
CHECK(actual.empty());
}
/* FIXME: This test is pretty weird. It would be much nicer if we could
* perform this operation without a TypeChecker so that we don't have to jam
* all this state into it to make stuff work.
*/
TEST_CASE_FIXTURE(Fixture, "substitution_skip_failure")
{
TypeVar ftv11{FreeTypeVar{TypeLevel{}}};
@ -257,6 +273,7 @@ TEST_CASE_FIXTURE(Fixture, "substitution_skip_failure")
TypeId root = &ttvTweenResult;
typeChecker.currentModule = std::make_shared<Module>();
typeChecker.currentModule->scopes.emplace_back(Location{}, std::make_shared<Scope>(getSingletonTypes().anyTypePack));
TypeId result = typeChecker.anyify(typeChecker.globalScope, root, Location{});

View file

@ -49,6 +49,12 @@ assert((function() _G.foo = 1 return _G['foo'] end)() == 1)
assert((function() _G['bar'] = 1 return _G.bar end)() == 1)
assert((function() local a = 1 (function () a = 2 end)() return a end)() == 2)
-- assignments with local conflicts
assert((function() local a, b = 1, {} a, b[a] = 43, -1 return a + b[1] end)() == 42)
assert((function() local a = {} local b = a a[1], a = 43, -1 return a + b[1] end)() == 42)
assert((function() local a, b = 1, {} a, b[a] = (function() return 43, -1 end)() return a + b[1] end)() == 42)
assert((function() local a = {} local b = a a[1], a = (function() return 43, -1 end)() return a + b[1] end)() == 42)
-- upvalues
assert((function() local a = 1 function foo() return a end return foo() end)() == 1)

View file

@ -100,6 +100,9 @@ assert(bit32.extract(0xa0001111, 28, 4) == 0xa)
assert(bit32.extract(0xa0001111, 31, 1) == 1)
assert(bit32.extract(0x50000111, 31, 1) == 0)
assert(bit32.extract(0xf2345679, 0, 32) == 0xf2345679)
assert(bit32.extract(0xa0001111, 16) == 0)
assert(bit32.extract(0xa0001111, 31) == 1)
assert(bit32.extract(42, 1, 3) == 5)
assert(not pcall(bit32.extract, 0, -1))
assert(not pcall(bit32.extract, 0, 32))
@ -152,5 +155,6 @@ assert(bit32.btest(1, "3") == true)
assert(bit32.btest("1", 3) == true)
assert(bit32.countlz("42") == 26)
assert(bit32.countrz("42") == 1)
assert(bit32.extract("42", 1, 3) == 5)
return('OK')

View file

@ -295,7 +295,7 @@ end
-- testing syntax limits
local syntaxdepth = if limitedstack then 200 else 1000
local syntaxdepth = if limitedstack then 200 else 500
local function testrep (init, rep)
local s = "local a; "..init .. string.rep(rep, syntaxdepth)
local a,b = loadstring(s)

View file

@ -145,6 +145,14 @@ end) == false)
assert(string.format("%*", "a\0b\0c") == "a\0b\0c")
assert(string.format("%*", string.rep("doge", 3000)) == string.rep("doge", 3000))
assert(string.format("%*", 42) == "42")
assert(string.format("%*", true) == "true")
assert(string.format("%*", setmetatable({}, { __tostring = function() return "ok" end })) == "ok")
local ud = newproxy(true)
getmetatable(ud).__tostring = function() return "good" end
assert(string.format("%*", ud) == "good")
assert(pcall(function()
string.format("%#*", "bad form")

View file

@ -1,13 +1,8 @@
AnnotationTests.as_expr_does_not_propagate_type_info
AnnotationTests.as_expr_is_bidirectional
AnnotationTests.as_expr_warns_on_unrelated_cast
AnnotationTests.builtin_types_are_not_exported
AnnotationTests.cannot_use_nonexported_type
AnnotationTests.cloned_interface_maintains_pointers_between_definitions
AnnotationTests.define_generic_type_alias
AnnotationTests.duplicate_type_param_name
AnnotationTests.for_loop_counter_annotation_is_checked
AnnotationTests.function_return_annotations_are_checked
AnnotationTests.generic_aliases_are_cloned_properly
AnnotationTests.interface_types_belong_to_interface_arena
AnnotationTests.luau_ice_triggers_an_ice
@ -15,41 +10,28 @@ AnnotationTests.luau_ice_triggers_an_ice_exception_with_flag
AnnotationTests.luau_ice_triggers_an_ice_exception_with_flag_handler
AnnotationTests.luau_ice_triggers_an_ice_handler
AnnotationTests.luau_print_is_magic_if_the_flag_is_set
AnnotationTests.luau_print_is_not_special_without_the_flag
AnnotationTests.occurs_check_on_cyclic_intersection_typevar
AnnotationTests.occurs_check_on_cyclic_union_typevar
AnnotationTests.self_referential_type_alias
AnnotationTests.too_many_type_params
AnnotationTests.two_type_params
AnnotationTests.type_annotations_inside_function_bodies
AnnotationTests.type_assertion_expr
AnnotationTests.unknown_type_reference_generates_error
AnnotationTests.use_type_required_from_another_file
AstQuery.last_argument_function_call_type
AstQuery::getDocumentationSymbolAtPosition.binding
AstQuery::getDocumentationSymbolAtPosition.event_callback_arg
AstQuery::getDocumentationSymbolAtPosition.overloaded_fn
AstQuery::getDocumentationSymbolAtPosition.prop
AutocompleteTest.argument_types
AutocompleteTest.arguments_to_global_lambda
AutocompleteTest.as_types
AutocompleteTest.autocomplete_boolean_singleton
AutocompleteTest.autocomplete_end_with_fn_exprs
AutocompleteTest.autocomplete_end_with_lambda
AutocompleteTest.autocomplete_first_function_arg_expected_type
AutocompleteTest.autocomplete_for_in_middle_keywords
AutocompleteTest.autocomplete_for_middle_keywords
AutocompleteTest.autocomplete_if_else_regression
AutocompleteTest.autocomplete_if_middle_keywords
AutocompleteTest.autocomplete_ifelse_expressions
AutocompleteTest.autocomplete_on_string_singletons
AutocompleteTest.autocomplete_oop_implicit_self
AutocompleteTest.autocomplete_repeat_middle_keyword
AutocompleteTest.autocomplete_string_singleton_equality
AutocompleteTest.autocomplete_string_singleton_escape
AutocompleteTest.autocomplete_string_singletons
AutocompleteTest.autocomplete_until_expression
AutocompleteTest.autocomplete_until_in_repeat
AutocompleteTest.autocomplete_while_middle_keywords
AutocompleteTest.autocompleteProp_index_function_metamethod_is_variadic
AutocompleteTest.bias_toward_inner_scope
@ -72,7 +54,6 @@ AutocompleteTest.get_suggestions_for_the_very_start_of_the_script
AutocompleteTest.global_function_params
AutocompleteTest.global_functions_are_not_scoped_lexically
AutocompleteTest.if_then_else_elseif_completions
AutocompleteTest.if_then_else_full_keywords
AutocompleteTest.keyword_methods
AutocompleteTest.keyword_types
AutocompleteTest.library_non_self_calls_are_fine
@ -127,7 +108,6 @@ BuiltinTests.assert_removes_falsy_types2
BuiltinTests.assert_removes_falsy_types_even_from_type_pack_tail_but_only_for_the_first_type
BuiltinTests.assert_returns_false_and_string_iff_it_knows_the_first_argument_cannot_be_truthy
BuiltinTests.bad_select_should_not_crash
BuiltinTests.builtin_tables_sealed
BuiltinTests.coroutine_resume_anything_goes
BuiltinTests.coroutine_wrap_anything_goes
BuiltinTests.debug_info_is_crazy
@ -136,28 +116,20 @@ BuiltinTests.dont_add_definitions_to_persistent_types
BuiltinTests.find_capture_types
BuiltinTests.find_capture_types2
BuiltinTests.find_capture_types3
BuiltinTests.gcinfo
BuiltinTests.getfenv
BuiltinTests.global_singleton_types_are_sealed
BuiltinTests.gmatch_capture_types
BuiltinTests.gmatch_capture_types2
BuiltinTests.gmatch_capture_types_balanced_escaped_parens
BuiltinTests.gmatch_capture_types_default_capture
BuiltinTests.gmatch_capture_types_invalid_pattern_fallback_to_builtin
BuiltinTests.gmatch_capture_types_invalid_pattern_fallback_to_builtin2
BuiltinTests.gmatch_capture_types_leading_end_bracket_is_part_of_set
BuiltinTests.gmatch_capture_types_parens_in_sets_are_ignored
BuiltinTests.gmatch_capture_types_set_containing_lbracket
BuiltinTests.gmatch_definition
BuiltinTests.ipairs_iterator_should_infer_types_and_type_check
BuiltinTests.lua_51_exported_globals_all_exist
BuiltinTests.match_capture_types
BuiltinTests.match_capture_types2
BuiltinTests.math_max_checks_for_numbers
BuiltinTests.math_max_variatic
BuiltinTests.math_things_are_defined
BuiltinTests.next_iterator_should_infer_types_and_type_check
BuiltinTests.no_persistent_typelevel_change
BuiltinTests.os_time_takes_optional_date_table
BuiltinTests.pairs_iterator_should_infer_types_and_type_check
BuiltinTests.see_thru_select
@ -170,7 +142,6 @@ BuiltinTests.select_with_variadic_typepack_tail
BuiltinTests.select_with_variadic_typepack_tail_and_string_head
BuiltinTests.set_metatable_needs_arguments
BuiltinTests.setmetatable_should_not_mutate_persisted_types
BuiltinTests.setmetatable_unpacks_arg_types_correctly
BuiltinTests.sort
BuiltinTests.sort_with_bad_predicate
BuiltinTests.sort_with_predicate
@ -179,6 +150,8 @@ BuiltinTests.string_format_arg_types_inference
BuiltinTests.string_format_as_method
BuiltinTests.string_format_correctly_ordered_types
BuiltinTests.string_format_report_all_type_errors_at_correct_positions
BuiltinTests.string_format_tostring_specifier
BuiltinTests.string_format_tostring_specifier_type_constraint
BuiltinTests.string_format_use_correct_argument
BuiltinTests.string_format_use_correct_argument2
BuiltinTests.string_lib_self_noself
@ -190,53 +163,36 @@ BuiltinTests.table_insert_correctly_infers_type_of_array_3_args_overload
BuiltinTests.table_pack
BuiltinTests.table_pack_reduce
BuiltinTests.table_pack_variadic
BuiltinTests.thread_is_a_type
BuiltinTests.tonumber_returns_optional_number_type
BuiltinTests.tonumber_returns_optional_number_type2
BuiltinTests.xpcall
DefinitionTests.class_definition_function_prop
DefinitionTests.declaring_generic_functions
DefinitionTests.definition_file_class_function_args
DefinitionTests.definition_file_classes
DefinitionTests.definition_file_loading
DefinitionTests.definitions_documentation_symbols
DefinitionTests.documentation_symbols_dont_attach_to_persistent_types
DefinitionTests.single_class_type_identity_in_global_types
FrontendTest.accumulate_cached_errors
FrontendTest.accumulate_cached_errors_in_consistent_order
FrontendTest.any_annotation_breaks_cycle
FrontendTest.ast_node_at_position
FrontendTest.automatically_check_cyclically_dependent_scripts
FrontendTest.automatically_check_dependent_scripts
FrontendTest.check_without_builtin_next
FrontendTest.clearStats
FrontendTest.cycle_detection_between_check_and_nocheck
FrontendTest.cycle_detection_disabled_in_nocheck
FrontendTest.cycle_error_paths
FrontendTest.cycle_errors_can_be_fixed
FrontendTest.cycle_incremental_type_surface
FrontendTest.cycle_incremental_type_surface_longer
FrontendTest.dont_recheck_script_that_hasnt_been_marked_dirty
FrontendTest.dont_reparse_clean_file_when_linting
FrontendTest.environments
FrontendTest.ignore_require_to_nonexistent_file
FrontendTest.imported_table_modification_2
FrontendTest.it_should_be_safe_to_stringify_errors_when_full_type_graph_is_discarded
FrontendTest.no_use_after_free_with_type_fun_instantiation
FrontendTest.nocheck_cycle_used_by_checked
FrontendTest.nocheck_modules_are_typed
FrontendTest.produce_errors_for_unchanged_file_with_a_syntax_error
FrontendTest.re_report_type_error_in_required_file
FrontendTest.recheck_if_dependent_script_is_dirty
FrontendTest.reexport_cyclic_type
FrontendTest.reexport_type_alias
FrontendTest.report_require_to_nonexistent_file
FrontendTest.report_syntax_error_in_required_file
FrontendTest.reports_errors_from_multiple_sources
FrontendTest.stats_are_not_reset_between_checks
FrontendTest.trace_requires_in_nonstrict_mode
GenericsTests.apply_type_function_nested_generics1
GenericsTests.apply_type_function_nested_generics2
GenericsTests.better_mismatch_error_messages
GenericsTests.bound_tables_do_not_clone_original_fields
GenericsTests.calling_self_generic_methods
GenericsTests.check_generic_typepack_function
GenericsTests.check_mutual_generic_functions
GenericsTests.correctly_instantiate_polymorphic_member_functions
@ -248,8 +204,6 @@ GenericsTests.duplicate_generic_types
GenericsTests.error_detailed_function_mismatch_generic_pack
GenericsTests.error_detailed_function_mismatch_generic_types
GenericsTests.factories_of_generics
GenericsTests.function_arguments_can_be_polytypes
GenericsTests.function_results_can_be_polytypes
GenericsTests.generic_argument_count_too_few
GenericsTests.generic_argument_count_too_many
GenericsTests.generic_factories
@ -265,8 +219,7 @@ GenericsTests.generic_type_pack_unification3
GenericsTests.infer_generic_function_function_argument
GenericsTests.infer_generic_function_function_argument_overloaded
GenericsTests.infer_generic_lib_function_function_argument
GenericsTests.infer_generic_property
GenericsTests.inferred_local_vars_can_be_polytypes
GenericsTests.infer_generic_methods
GenericsTests.instantiate_cyclic_generic_function
GenericsTests.instantiate_generic_function_in_assignments
GenericsTests.instantiate_generic_function_in_assignments2
@ -276,41 +229,32 @@ GenericsTests.local_vars_can_be_instantiated_polytypes
GenericsTests.mutable_state_polymorphism
GenericsTests.no_stack_overflow_from_quantifying
GenericsTests.properties_can_be_instantiated_polytypes
GenericsTests.properties_can_be_polytypes
GenericsTests.rank_N_types_via_typeof
GenericsTests.reject_clashing_generic_and_pack_names
GenericsTests.self_recursive_instantiated_param
GenericsTests.variadic_generics
IntersectionTypes.argument_is_intersection
IntersectionTypes.error_detailed_intersection_all
IntersectionTypes.error_detailed_intersection_part
IntersectionTypes.fx_intersection_as_argument
IntersectionTypes.fx_union_as_argument_fails
IntersectionTypes.index_on_an_intersection_type_with_mixed_types
IntersectionTypes.index_on_an_intersection_type_with_one_part_missing_the_property
IntersectionTypes.index_on_an_intersection_type_with_one_property_of_type_any
IntersectionTypes.index_on_an_intersection_type_with_property_guaranteed_to_exist
IntersectionTypes.index_on_an_intersection_type_works_at_arbitrary_depth
IntersectionTypes.no_stack_overflow_from_flattenintersection
IntersectionTypes.overload_is_not_a_function
IntersectionTypes.select_correct_union_fn
IntersectionTypes.should_still_pick_an_overload_whose_arguments_are_unions
IntersectionTypes.table_intersection_setmetatable
IntersectionTypes.table_intersection_write
IntersectionTypes.table_intersection_write_sealed
IntersectionTypes.table_intersection_write_sealed_indirect
IntersectionTypes.table_write_sealed_indirect
isSubtype.functions_and_any
isSubtype.intersection_of_functions_of_different_arities
isSubtype.intersection_of_tables
isSubtype.table_with_any_prop
isSubtype.table_with_table_prop
isSubtype.tables
Linter.DeprecatedApi
Linter.TableOperations
ModuleTests.builtin_types_point_into_globalTypes_arena
ModuleTests.clone_self_property
ModuleTests.deepClone_cyclic_table
ModuleTests.do_not_clone_reexports
ModuleTests.do_not_clone_types_of_reexported_values
NonstrictModeTests.delay_function_does_not_require_its_argument_to_return_anything
NonstrictModeTests.for_in_iterator_variables_are_any
NonstrictModeTests.function_parameters_are_any
@ -333,8 +277,6 @@ Normalize.cyclic_intersection
Normalize.cyclic_table_normalizes_sensibly
Normalize.cyclic_union
Normalize.fuzz_failure_bound_type_is_normal_but_not_its_bounded_to
Normalize.fuzz_failure_instersection_combine_must_follow
Normalize.higher_order_function
Normalize.intersection_combine_on_bound_self
Normalize.intersection_inside_a_table_inside_another_intersection
Normalize.intersection_inside_a_table_inside_another_intersection_2
@ -345,13 +287,10 @@ Normalize.intersection_of_disjoint_tables
Normalize.intersection_of_functions
Normalize.intersection_of_overlapping_tables
Normalize.intersection_of_tables_with_indexers
Normalize.nested_table_normalization_with_non_table__no_ice
Normalize.normalization_does_not_convert_ever
Normalize.normalize_module_return_type
Normalize.normalize_unions_containing_never
Normalize.normalize_unions_containing_unknown
Normalize.return_type_is_not_a_constrained_intersection
Normalize.skip_force_normal_on_external_types
Normalize.union_of_distinct_free_types
Normalize.variadic_tail_is_marked_normal
Normalize.visiting_a_type_twice_is_not_considered_normal
@ -365,7 +304,6 @@ ProvisionalTests.constrained_is_level_dependent
ProvisionalTests.discriminate_from_x_not_equal_to_nil
ProvisionalTests.do_not_ice_when_trying_to_pick_first_of_generic_type_pack
ProvisionalTests.error_on_eq_metamethod_returning_a_type_other_than_boolean
ProvisionalTests.free_is_not_bound_to_any
ProvisionalTests.function_returns_many_things_but_first_of_it_is_forgotten
ProvisionalTests.greedy_inference_with_shared_self_triggers_function_with_no_returns
ProvisionalTests.invariant_table_properties_means_instantiating_tables_in_call_is_unsound
@ -380,7 +318,6 @@ ProvisionalTests.typeguard_inference_incomplete
ProvisionalTests.weird_fail_to_unify_type_pack
ProvisionalTests.weirditer_should_not_loop_forever
ProvisionalTests.while_body_are_also_refined
ProvisionalTests.xpcall_returns_what_f_returns
RefinementTest.and_constraint
RefinementTest.and_or_peephole_refinement
RefinementTest.apply_refinements_on_astexprindexexpr_whose_subscript_expr_is_constant_string
@ -420,7 +357,6 @@ RefinementTest.not_and_constraint
RefinementTest.not_t_or_some_prop_of_t
RefinementTest.or_predicate_with_truthy_predicates
RefinementTest.parenthesized_expressions_are_followed_through
RefinementTest.refine_a_property_not_to_be_nil_through_an_intersection_table
RefinementTest.refine_the_correct_types_opposite_of_when_a_is_not_number_or_string
RefinementTest.refine_unknowns
RefinementTest.string_not_equal_to_string_or_nil
@ -456,7 +392,6 @@ TableTests.augment_nested_table
TableTests.augment_table
TableTests.builtin_table_names
TableTests.call_method
TableTests.call_method_with_explicit_self_argument
TableTests.cannot_augment_sealed_table
TableTests.cannot_call_tables
TableTests.cannot_change_type_of_unsealed_table_prop
@ -469,16 +404,13 @@ TableTests.common_table_element_union_in_call_tail
TableTests.confusing_indexing
TableTests.defining_a_method_for_a_builtin_sealed_table_must_fail
TableTests.defining_a_method_for_a_local_sealed_table_must_fail
TableTests.defining_a_method_for_a_local_unsealed_table_is_ok
TableTests.defining_a_self_method_for_a_builtin_sealed_table_must_fail
TableTests.defining_a_self_method_for_a_local_sealed_table_must_fail
TableTests.defining_a_self_method_for_a_local_unsealed_table_is_ok
TableTests.dont_crash_when_setmetatable_does_not_produce_a_metatabletypevar
TableTests.dont_hang_when_trying_to_look_up_in_cyclic_metatable_index
TableTests.dont_invalidate_the_properties_iterator_of_free_table_when_rolled_back
TableTests.dont_leak_free_table_props
TableTests.dont_quantify_table_that_belongs_to_outer_scope
TableTests.dont_seal_an_unsealed_table_by_passing_it_to_a_function_that_takes_a_sealed_table
TableTests.dont_suggest_exact_match_keys
TableTests.error_detailed_indexer_key
TableTests.error_detailed_indexer_value
@ -508,24 +440,19 @@ TableTests.infer_array_2
TableTests.infer_indexer_from_value_property_in_literal
TableTests.inferred_return_type_of_free_table
TableTests.inferring_crazy_table_should_also_be_quick
TableTests.instantiate_table_cloning
TableTests.instantiate_table_cloning_2
TableTests.instantiate_table_cloning_3
TableTests.instantiate_tables_at_scope_level
TableTests.invariant_table_properties_means_instantiating_tables_in_assignment_is_unsound
TableTests.leaking_bad_metatable_errors
TableTests.length_operator_intersection
TableTests.length_operator_non_table_union
TableTests.length_operator_union
TableTests.length_operator_union_errors
TableTests.less_exponential_blowup_please
TableTests.meta_add
TableTests.meta_add_both_ways
TableTests.meta_add_inferred
TableTests.metatable_mismatch_should_fail
TableTests.missing_metatable_for_sealed_tables_do_not_get_inferred
TableTests.mixed_tables_with_implicit_numbered_keys
TableTests.MixedPropertiesAndIndexers
TableTests.nil_assign_doesnt_hit_indexer
TableTests.okay_to_add_property_to_unsealed_tables_by_function_call
TableTests.only_ascribe_synthetic_names_at_module_scope
@ -535,8 +462,8 @@ TableTests.open_table_unification_2
TableTests.pass_a_union_of_tables_to_a_function_that_requires_a_table
TableTests.pass_a_union_of_tables_to_a_function_that_requires_a_table_2
TableTests.pass_incompatible_union_to_a_generic_table_without_crashing
TableTests.passing_compatible_unions_to_a_generic_table_without_crashing
TableTests.persistent_sealed_table_is_immutable
TableTests.prop_access_on_key_whose_types_mismatches
TableTests.property_lookup_through_tabletypevar_metatable
TableTests.quantify_even_that_table_was_never_exported_at_all
TableTests.quantify_metatables_of_metatables_of_table
@ -570,23 +497,16 @@ TableTests.tc_member_function_2
TableTests.top_table_type
TableTests.type_mismatch_on_massive_table_is_cut_short
TableTests.unification_of_unions_in_a_self_referential_type
TableTests.unifying_tables_shouldnt_uaf1
TableTests.unifying_tables_shouldnt_uaf2
TableTests.used_colon_correctly
TableTests.used_colon_instead_of_dot
TableTests.used_dot_instead_of_colon
TableTests.used_dot_instead_of_colon_but_correctly
TableTests.width_subtyping
ToDot.bound_table
ToDot.class
ToDot.function
ToDot.metatable
ToDot.primitive
ToDot.table
ToString.exhaustive_toString_of_cyclic_table
ToString.function_type_with_argument_names_and_self
ToString.function_type_with_argument_names_generic
ToString.named_metatable_toStringNamedFunction
ToString.no_parentheses_around_cyclic_function_type_in_union
ToString.toStringDetailed2
ToString.toStringErrorPack
@ -605,13 +525,12 @@ TryUnifyTests.typepack_unification_should_trim_free_tails
TryUnifyTests.variadics_should_use_reversed_properly
TypeAliases.cli_38393_recursive_intersection_oom
TypeAliases.corecursive_types_generic
TypeAliases.do_not_quantify_unresolved_aliases
TypeAliases.forward_declared_alias_is_not_clobbered_by_prior_unification_with_any
TypeAliases.general_require_multi_assign
TypeAliases.generic_param_remap
TypeAliases.mismatched_generic_pack_type_param
TypeAliases.mismatched_generic_type_param
TypeAliases.mutually_recursive_generic_aliases
TypeAliases.mutually_recursive_types_errors
TypeAliases.mutually_recursive_types_restriction_not_ok_1
TypeAliases.mutually_recursive_types_restriction_not_ok_2
TypeAliases.mutually_recursive_types_swapsies_not_ok
@ -619,29 +538,19 @@ TypeAliases.recursive_types_restriction_not_ok
TypeAliases.stringify_optional_parameterized_alias
TypeAliases.stringify_type_alias_of_recursive_template_table_type
TypeAliases.stringify_type_alias_of_recursive_template_table_type2
TypeAliases.type_alias_import_mutation
TypeAliases.type_alias_fwd_declaration_is_precise
TypeAliases.type_alias_local_mutation
TypeAliases.type_alias_local_rename
TypeAliases.type_alias_of_an_imported_recursive_generic_type
TypeAliases.type_alias_of_an_imported_recursive_type
TypeInfer.check_expr_recursion_limit
TypeInfer.checking_should_not_ice
TypeInfer.cli_50041_committing_txnlog_in_apollo_client_error
TypeInfer.cyclic_follow
TypeInfer.do_not_bind_a_free_table_to_a_union_containing_that_table
TypeInfer.dont_report_type_errors_within_an_AstStatError
TypeInfer.follow_on_new_types_in_substitution
TypeInfer.free_typevars_introduced_within_control_flow_constructs_do_not_get_an_elevated_TypeLevel
TypeInfer.globals
TypeInfer.globals2
TypeInfer.index_expr_should_be_checked
TypeInfer.infer_assignment_value_types
TypeInfer.infer_assignment_value_types_mutable_lval
TypeInfer.infer_through_group_expr
TypeInfer.infer_type_assertion_value_type
TypeInfer.no_heap_use_after_free_error
TypeInfer.no_stack_overflow_from_isoptional
TypeInfer.recursive_metatable_crash
TypeInfer.tc_after_error_recovery_no_replacement_name_in_error
TypeInfer.tc_if_else_expressions1
TypeInfer.tc_if_else_expressions2
@ -650,82 +559,50 @@ TypeInfer.tc_if_else_expressions_expected_type_2
TypeInfer.tc_if_else_expressions_expected_type_3
TypeInfer.tc_if_else_expressions_type_union
TypeInfer.type_infer_recursion_limit_no_ice
TypeInfer.types stored in astResolvedTypes
TypeInfer.warn_on_lowercase_parent_property
TypeInfer.weird_case
TypeInferAnyError.any_type_propagates
TypeInferAnyError.assign_prop_to_table_by_calling_any_yields_any
TypeInferAnyError.call_to_any_yields_any
TypeInferAnyError.calling_error_type_yields_error
TypeInferAnyError.can_get_length_of_any
TypeInferAnyError.can_subscript_any
TypeInferAnyError.CheckMethodsOfAny
TypeInferAnyError.for_in_loop_iterator_is_any
TypeInferAnyError.for_in_loop_iterator_is_any2
TypeInferAnyError.for_in_loop_iterator_is_error
TypeInferAnyError.for_in_loop_iterator_is_error2
TypeInferAnyError.for_in_loop_iterator_returns_any
TypeInferAnyError.for_in_loop_iterator_returns_any2
TypeInferAnyError.indexing_error_type_does_not_produce_an_error
TypeInferAnyError.length_of_error_type_does_not_produce_an_error
TypeInferAnyError.metatable_of_any_can_be_a_table
TypeInferAnyError.prop_access_on_any_with_other_options
TypeInferAnyError.quantify_any_does_not_bind_to_itself
TypeInferAnyError.replace_every_free_type_when_unifying_a_complex_function_with_any
TypeInferAnyError.type_error_addition
TypeInferClasses.assign_to_prop_of_class
TypeInferClasses.call_base_method
TypeInferClasses.call_instance_method
TypeInferClasses.call_method_of_a_child_class
TypeInferClasses.call_method_of_a_class
TypeInferClasses.can_assign_to_prop_of_base_class
TypeInferClasses.can_assign_to_prop_of_base_class_using_string
TypeInferClasses.can_read_prop_of_base_class
TypeInferClasses.can_read_prop_of_base_class_using_string
TypeInferClasses.cannot_call_method_of_child_on_base_instance
TypeInferClasses.cannot_call_unknown_method_of_a_class
TypeInferClasses.cannot_unify_class_instance_with_primitive
TypeInferClasses.class_type_mismatch_with_name_conflict
TypeInferClasses.class_unification_type_mismatch_is_correct_order
TypeInferClasses.classes_can_have_overloaded_operators
TypeInferClasses.classes_without_overloaded_operators_cannot_be_added
TypeInferClasses.detailed_class_unification_error
TypeInferClasses.function_arguments_are_covariant
TypeInferClasses.higher_order_function_arguments_are_contravariant
TypeInferClasses.higher_order_function_return_type_is_not_contravariant
TypeInferClasses.higher_order_function_return_values_are_covariant
TypeInferClasses.optional_class_field_access_error
TypeInferClasses.table_class_unification_reports_sane_errors_for_missing_properties
TypeInferClasses.table_indexers_are_invariant
TypeInferClasses.table_properties_are_invariant
TypeInferClasses.warn_when_prop_almost_matches
TypeInferClasses.we_can_infer_that_a_parameter_must_be_a_particular_class
TypeInferClasses.we_can_report_when_someone_is_trying_to_use_a_table_rather_than_a_class
TypeInferFunctions.another_indirect_function_case_where_it_is_ok_to_provide_too_many_arguments
TypeInferFunctions.another_recursive_local_function
TypeInferFunctions.calling_function_with_anytypepack_doesnt_leak_free_types
TypeInferFunctions.calling_function_with_incorrect_argument_type_yields_errors_spanning_argument
TypeInferFunctions.cannot_hoist_interior_defns_into_signature
TypeInferFunctions.check_function_before_lambda_that_uses_it
TypeInferFunctions.complicated_return_types_require_an_explicit_annotation
TypeInferFunctions.cyclic_function_type_in_args
TypeInferFunctions.dont_give_other_overloads_message_if_only_one_argument_matching_overload_exists
TypeInferFunctions.dont_infer_parameter_types_for_functions_from_their_call_site
TypeInferFunctions.dont_mutate_the_underlying_head_of_typepack_when_calling_with_self
TypeInferFunctions.duplicate_functions_with_different_signatures_not_allowed_in_nonstrict
TypeInferFunctions.error_detailed_function_mismatch_arg
TypeInferFunctions.error_detailed_function_mismatch_arg_count
TypeInferFunctions.error_detailed_function_mismatch_ret
TypeInferFunctions.error_detailed_function_mismatch_ret_count
TypeInferFunctions.error_detailed_function_mismatch_ret_mult
TypeInferFunctions.first_argument_can_be_optional
TypeInferFunctions.free_is_not_bound_to_unknown
TypeInferFunctions.func_expr_doesnt_leak_free
TypeInferFunctions.function_cast_error_uses_correct_language
TypeInferFunctions.function_decl_non_self_sealed_overwrite
TypeInferFunctions.function_decl_non_self_sealed_overwrite_2
TypeInferFunctions.function_decl_non_self_unsealed_overwrite
TypeInferFunctions.function_decl_quantify_right_type
TypeInferFunctions.function_does_not_return_enough_values
TypeInferFunctions.function_statement_sealed_table_assignment_through_indexer
TypeInferFunctions.higher_order_function_2
@ -734,16 +611,12 @@ TypeInferFunctions.ignored_return_values
TypeInferFunctions.inconsistent_higher_order_function
TypeInferFunctions.inconsistent_return_types
TypeInferFunctions.infer_anonymous_function_arguments
TypeInferFunctions.infer_anonymous_function_arguments_outside_call
TypeInferFunctions.infer_return_type_from_selected_overload
TypeInferFunctions.infer_that_function_does_not_return_a_table
TypeInferFunctions.inferred_higher_order_functions_are_quantified_at_the_right_time
TypeInferFunctions.inferred_higher_order_functions_are_quantified_at_the_right_time2
TypeInferFunctions.it_is_ok_not_to_supply_enough_retvals
TypeInferFunctions.it_is_ok_to_oversaturate_a_higher_order_function_argument
TypeInferFunctions.list_all_overloads_if_no_overload_takes_given_argument_count
TypeInferFunctions.list_only_alternative_overloads_that_match_argument_count
TypeInferFunctions.mutual_recursion
TypeInferFunctions.no_lossy_function_type
TypeInferFunctions.occurs_check_failure_in_function_return_type
TypeInferFunctions.quantify_constrained_types
@ -759,10 +632,8 @@ TypeInferFunctions.too_few_arguments_variadic_generic
TypeInferFunctions.too_few_arguments_variadic_generic2
TypeInferFunctions.too_many_arguments
TypeInferFunctions.too_many_return_values
TypeInferFunctions.toposort_doesnt_break_mutual_recursion
TypeInferFunctions.vararg_function_is_quantified
TypeInferFunctions.vararg_functions_should_allow_calls_of_any_types_and_size
TypeInferLoops.correctly_scope_locals_while
TypeInferLoops.for_in_loop
TypeInferLoops.for_in_loop_error_on_factory_not_returning_the_right_amount_of_values
TypeInferLoops.for_in_loop_error_on_iterator_requiring_args_but_none_given
@ -784,19 +655,11 @@ TypeInferLoops.loop_iter_no_indexer_strict
TypeInferLoops.loop_iter_trailing_nil
TypeInferLoops.loop_typecheck_crash_on_empty_optional
TypeInferLoops.properly_infer_iteratee_is_a_free_table
TypeInferLoops.repeat_loop
TypeInferLoops.repeat_loop_condition_binds_to_its_block
TypeInferLoops.symbols_in_repeat_block_should_not_be_visible_beyond_until_condition
TypeInferLoops.unreachable_code_after_infinite_loop
TypeInferLoops.varlist_declared_by_for_in_loop_should_be_free
TypeInferLoops.while_loop
TypeInferModules.bound_free_table_export_is_ok
TypeInferModules.constrained_anyification_clone_immutable_types
TypeInferModules.custom_require_global
TypeInferModules.do_not_modify_imported_types
TypeInferModules.do_not_modify_imported_types_2
TypeInferModules.do_not_modify_imported_types_3
TypeInferModules.do_not_modify_imported_types_4
TypeInferModules.general_require_call_expression
TypeInferModules.general_require_type_mismatch
TypeInferModules.module_type_conflict
@ -808,16 +671,14 @@ TypeInferModules.require_module_that_does_not_export
TypeInferModules.require_types
TypeInferModules.type_error_of_unknown_qualified_type
TypeInferModules.warn_if_you_try_to_require_a_non_modulescript
TypeInferOOP.CheckMethodsOfSealed
TypeInferOOP.dont_suggest_using_colon_rather_than_dot_if_another_overload_works
TypeInferOOP.dont_suggest_using_colon_rather_than_dot_if_it_wont_help_2
TypeInferOOP.dont_suggest_using_colon_rather_than_dot_if_not_defined_with_colon
TypeInferOOP.inferred_methods_of_free_tables_have_the_same_level_as_the_enclosing_table
TypeInferOOP.inferring_hundreds_of_self_calls_should_not_suffocate_memory
TypeInferOOP.method_depends_on_table
TypeInferOOP.methods_are_topologically_sorted
TypeInferOOP.nonstrict_self_mismatch_tail
TypeInferOOP.object_constructor_can_refer_to_method_of_self
TypeInferOOP.table_oop
TypeInferOperators.and_adds_boolean
TypeInferOperators.and_adds_boolean_no_superfluous_union
TypeInferOperators.and_binexps_dont_unify
@ -829,8 +690,6 @@ TypeInferOperators.cannot_indirectly_compare_types_that_do_not_offer_overloaded_
TypeInferOperators.cli_38355_recursive_union
TypeInferOperators.compare_numbers
TypeInferOperators.compare_strings
TypeInferOperators.compound_assign_basic
TypeInferOperators.compound_assign_metatable
TypeInferOperators.compound_assign_mismatch_metatable
TypeInferOperators.compound_assign_mismatch_op
TypeInferOperators.compound_assign_mismatch_result
@ -872,37 +731,25 @@ TypeInferPrimitives.string_function_other
TypeInferPrimitives.string_index
TypeInferPrimitives.string_length
TypeInferPrimitives.string_method
TypeInferUnknownNever.array_like_table_of_never_is_inhabitable
TypeInferUnknownNever.assign_to_global_which_is_never
TypeInferUnknownNever.assign_to_local_which_is_never
TypeInferUnknownNever.assign_to_prop_which_is_never
TypeInferUnknownNever.assign_to_subscript_which_is_never
TypeInferUnknownNever.call_never
TypeInferUnknownNever.dont_unify_operands_if_one_of_the_operand_is_never_in_any_ordering_operators
TypeInferUnknownNever.index_on_never
TypeInferUnknownNever.index_on_union_of_tables_for_properties_that_is_never
TypeInferUnknownNever.index_on_union_of_tables_for_properties_that_is_sorta_never
TypeInferUnknownNever.length_of_never
TypeInferUnknownNever.math_operators_and_never
TypeInferUnknownNever.never_is_reflexive
TypeInferUnknownNever.never_subtype_and_string_supertype
TypeInferUnknownNever.pick_never_from_variadic_type_pack
TypeInferUnknownNever.string_subtype_and_never_supertype
TypeInferUnknownNever.string_subtype_and_unknown_supertype
TypeInferUnknownNever.table_with_prop_of_type_never_is_also_reflexive
TypeInferUnknownNever.table_with_prop_of_type_never_is_uninhabitable
TypeInferUnknownNever.type_packs_containing_never_is_itself_uninhabitable
TypeInferUnknownNever.type_packs_containing_never_is_itself_uninhabitable2
TypeInferUnknownNever.unary_minus_of_never
TypeInferUnknownNever.unknown_is_reflexive
TypeInferUnknownNever.unknown_subtype_and_string_supertype
TypePackTests.cyclic_type_packs
TypePackTests.higher_order_function
TypePackTests.multiple_varargs_inference_are_not_confused
TypePackTests.no_return_size_should_be_zero
TypePackTests.pack_tail_unification_check
TypePackTests.parenthesized_varargs_returns_any
TypePackTests.self_and_varargs_should_work
TypePackTests.type_alias_backwards_compatible
TypePackTests.type_alias_default_export
TypePackTests.type_alias_default_mixed_self
@ -913,8 +760,6 @@ TypePackTests.type_alias_default_type_pack_self_tp
TypePackTests.type_alias_default_type_self
TypePackTests.type_alias_defaults_confusing_types
TypePackTests.type_alias_defaults_recursive_type
TypePackTests.type_alias_type_pack_explicit
TypePackTests.type_alias_type_pack_explicit_multi
TypePackTests.type_alias_type_pack_multi
TypePackTests.type_alias_type_pack_variadic
TypePackTests.type_alias_type_packs
@ -949,6 +794,7 @@ TypeSingletons.string_singletons_escape_chars
TypeSingletons.string_singletons_mismatch
TypeSingletons.table_insert_with_a_singleton_argument
TypeSingletons.table_properties_type_error_escapes
TypeSingletons.tagged_unions_immutable_tag
TypeSingletons.tagged_unions_using_singletons
TypeSingletons.taking_the_length_of_string_singleton
TypeSingletons.taking_the_length_of_union_of_string_singleton
@ -978,5 +824,4 @@ UnionTypes.optional_union_members
UnionTypes.optional_union_methods
UnionTypes.return_types_can_be_disjoint
UnionTypes.table_union_write_indirect
UnionTypes.unify_unsealed_table_union_check
UnionTypes.union_equality_comparisons

View file

@ -39,6 +39,16 @@ class Node(svg.Node):
def details(self, root):
return "{} ({:,} bytes, {:.1%}); self: {:,} bytes in {:,} objects".format(self.name, self.width, self.width / root.width, self.size, self.count)
def getkey(heap, obj, key):
pairs = obj.get("pairs", [])
for i in range(0, len(pairs), 2):
if pairs[i] and heap[pairs[i]]["type"] == "string" and heap[pairs[i]]["data"] == key:
if pairs[i + 1] and heap[pairs[i + 1]]["type"] == "string":
return heap[pairs[i + 1]]["data"]
else:
return None
return None
# load files
if arguments.snapshotnew == None:
dumpold = None
@ -50,6 +60,8 @@ else:
with open(arguments.snapshotnew) as f:
dump = json.load(f)
heap = dump["objects"]
# reachability analysis: how much of the heap is reachable from roots?
visited = set()
queue = []
@ -66,7 +78,7 @@ while offset < len(queue):
continue
visited.add(addr)
obj = dump["objects"][addr]
obj = heap[addr]
if not dumpold or not addr in dumpold["objects"]:
node.count += 1
@ -75,17 +87,27 @@ while offset < len(queue):
if obj["type"] == "table":
pairs = obj.get("pairs", [])
weakkey = False
weakval = False
if "metatable" in obj:
modemt = getkey(heap, heap[obj["metatable"]], "__mode")
if modemt:
weakkey = "k" in modemt
weakval = "v" in modemt
for i in range(0, len(pairs), 2):
key = pairs[i+0]
val = pairs[i+1]
if key and val and dump["objects"][key]["type"] == "string":
if key and heap[key]["type"] == "string":
# string keys are always strong
queue.append((key, node))
queue.append((val, node.child(dump["objects"][key]["data"])))
if val and not weakval:
queue.append((val, node.child(heap[key]["data"])))
else:
if key:
if key and not weakkey:
queue.append((key, node))
if val:
if val and not weakval:
queue.append((val, node))
for a in obj.get("array", []):
@ -97,7 +119,7 @@ while offset < len(queue):
source = ""
if "proto" in obj:
proto = dump["objects"][obj["proto"]]
proto = heap[obj["proto"]]
if "source" in proto:
source = proto["source"]
@ -110,8 +132,16 @@ while offset < len(queue):
queue.append((obj["metatable"], node.child("__meta")))
elif obj["type"] == "thread":
queue.append((obj["env"], node.child("__env")))
for a in obj.get("stack", []):
queue.append((a, node.child("__stack")))
stack = obj.get("stack")
stacknames = obj.get("stacknames", [])
stacknode = node.child("__stack")
framenode = None
for i in range(len(stack)):
name = stacknames[i] if stacknames else None
if name and name.startswith("frame:"):
framenode = stacknode.child(name[6:])
name = None
queue.append((stack[i], framenode.child(name) if framenode and name else framenode or stacknode))
elif obj["type"] == "proto":
for a in obj.get("constants", []):
queue.append((a, node))

View file

@ -15,12 +15,20 @@ def updatesize(d, k, s):
def sortedsize(p):
return sorted(p, key = lambda s: s[1][1], reverse = True)
def getkey(heap, obj, key):
pairs = obj.get("pairs", [])
for i in range(0, len(pairs), 2):
if pairs[i] and heap[pairs[i]]["type"] == "string" and heap[pairs[i]]["data"] == key:
if pairs[i + 1] and heap[pairs[i + 1]]["type"] == "string":
return heap[pairs[i + 1]]["data"]
else:
return None
return None
with open(sys.argv[1]) as f:
dump = json.load(f)
heap = dump["objects"]
type_addr = next((addr for addr,obj in heap.items() if obj["type"] == "string" and obj["data"] == "__type"), None)
size_type = {}
size_udata = {}
size_category = {}
@ -33,11 +41,7 @@ for addr, obj in heap.items():
if obj["type"] == "userdata" and "metatable" in obj:
metatable = heap[obj["metatable"]]
pairs = metatable.get("pairs", [])
typemt = "unknown"
for i in range(0, len(pairs), 2):
if type_addr and pairs[i] == type_addr and pairs[i + 1] and heap[pairs[i + 1]]["type"] == "string":
typemt = heap[pairs[i + 1]]["data"]
typemt = getkey(heap, metatable, "__type") or "unknown"
updatesize(size_udata, typemt, obj["size"])
print("objects by type:")
@ -55,5 +59,6 @@ if len(size_category) != 0:
print("objects by category:")
for type, (count, size) in sortedsize(size_category.items()):
name = dump["stats"]["categories"][type]["name"]
cat = dump["stats"]["categories"][type]
name = cat["name"] if "name" in cat else str(type)
print(name.ljust(30), str(size).rjust(8), "bytes", str(count).rjust(5), "objects")

View file

@ -56,18 +56,28 @@ def nodeFromCallstackListFile(source_file):
return root
def getDuration(obj):
total = obj['TotalDuration']
def nodeFromJSONbject(node, key, obj):
if 'Children' in obj:
for key, obj in obj['Children'].items():
total -= obj['TotalDuration']
return total
def nodeFromJSONObject(node, key, obj):
source, function, line = key.split(",")
node.function = function
node.source = source
node.line = int(line) if len(line) > 0 else 0
node.ticks = obj['Duration']
node.ticks = getDuration(obj)
for key, obj in obj['Children'].items():
nodeFromJSONbject(node.child(key), key, obj)
if 'Children' in obj:
for key, obj in obj['Children'].items():
nodeFromJSONObject(node.child(key), key, obj)
return node
@ -77,8 +87,9 @@ def nodeFromJSONFile(source_file):
root = Node()
for key, obj in dump['Children'].items():
nodeFromJSONbject(root.child(key), key, obj)
if 'Children' in dump:
for key, obj in dump['Children'].items():
nodeFromJSONObject(root.child(key), key, obj)
return root