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: jobs:
callgrind: callgrind:
name: callgrind ${{ matrix.compiler }}
strategy: strategy:
fail-fast: false
matrix: matrix:
os: [ubuntu-22.04] os: [ubuntu-22.04]
compiler: [g++]
benchResultsRepo: benchResultsRepo:
- { name: "luau-lang/benchmark-data", branch: "main" } - { name: "luau-lang/benchmark-data", branch: "main" }
@ -31,8 +28,19 @@ jobs:
run: | run: |
sudo apt-get install valgrind sudo apt-get install valgrind
- name: Build Luau - name: Build Luau (gcc)
run: CXX=${{ matrix.compiler }} make config=release CALLGRIND=1 luau luau-analyze 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) - name: Run benchmark (bench)
run: | run: |
@ -71,11 +79,19 @@ jobs:
- name: Store results (bench) - name: Store results (bench)
uses: Roblox/rhysd-github-action-benchmark@v-luau uses: Roblox/rhysd-github-action-benchmark@v-luau
with: with:
name: callgrind ${{ matrix.compiler }} name: callgrind clang
tool: "benchmarkluau" tool: "benchmarkluau"
output-file-path: ./bench-output.txt output-file-path: ./bench-output.txt
external-data-json-path: ./gh-pages/bench.json 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) - name: Store results (analyze)
uses: Roblox/rhysd-github-action-benchmark@v-luau uses: Roblox/rhysd-github-action-benchmark@v-luau
with: 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>; using ModulePtr = std::shared_ptr<Module>;
enum class AutocompleteContext
{
Unknown,
Expression,
Statement,
Property,
Type,
Keyword,
String,
};
enum class AutocompleteEntryKind enum class AutocompleteEntryKind
{ {
Property, Property,
@ -66,11 +77,13 @@ struct AutocompleteResult
{ {
AutocompleteEntryMap entryMap; AutocompleteEntryMap entryMap;
std::vector<AstNode*> ancestry; std::vector<AstNode*> ancestry;
AutocompleteContext context = AutocompleteContext::Unknown;
AutocompleteResult() = default; AutocompleteResult() = default;
AutocompleteResult(AutocompleteEntryMap entryMap, std::vector<AstNode*> ancestry) AutocompleteResult(AutocompleteEntryMap entryMap, std::vector<AstNode*> ancestry, AutocompleteContext context)
: entryMap(std::move(entryMap)) : entryMap(std::move(entryMap))
, ancestry(std::move(ancestry)) , 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); TypeId clone(TypeId tp, TypeArena& dest, CloneState& cloneState);
TypeFun clone(const TypeFun& typeFun, 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 } // namespace Luau

View file

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

View file

@ -28,6 +28,7 @@ struct ConstraintGraphBuilder
std::vector<std::pair<Location, ScopePtr>> scopes; std::vector<std::pair<Location, ScopePtr>> scopes;
ModuleName moduleName; ModuleName moduleName;
ModulePtr module;
SingletonTypes& singletonTypes; SingletonTypes& singletonTypes;
const NotNull<TypeArena> arena; const NotNull<TypeArena> arena;
// The root scope of the module we're generating constraints for. // 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. // Occasionally constraint generation needs to produce an ICE.
const NotNull<InternalErrorReporter> 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. * 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. * 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. * @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. * 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, AstStatBlock* block);
void visit(const ScopePtr& scope, AstStatLocal* local); void visit(const ScopePtr& scope, AstStatLocal* local);
void visit(const ScopePtr& scope, AstStatFor* for_); 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, AstStatLocalFunction* function);
void visit(const ScopePtr& scope, AstStatFunction* function); void visit(const ScopePtr& scope, AstStatFunction* function);
void visit(const ScopePtr& scope, AstStatReturn* ret); void visit(const ScopePtr& scope, AstStatReturn* ret);
void visit(const ScopePtr& scope, AstStatAssign* assign); 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, AstStatIf* ifStatement);
void visit(const ScopePtr& scope, AstStatTypeAlias* alias); void visit(const ScopePtr& scope, AstStatTypeAlias* alias);
void visit(const ScopePtr& scope, AstStatDeclareGlobal* declareGlobal); 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, AstExprIndexExpr* indexExpr);
TypeId check(const ScopePtr& scope, AstExprUnary* unary); TypeId check(const ScopePtr& scope, AstExprUnary* unary);
TypeId check(const ScopePtr& scope, AstExprBinary* binary); TypeId check(const ScopePtr& scope, AstExprBinary* binary);
TypeId check(const ScopePtr& scope, AstExprIfElse* ifElse);
TypeId check(const ScopePtr& scope, AstExprTypeAssertion* typeAssert);
struct FunctionSignature struct FunctionSignature
{ {

View file

@ -60,6 +60,9 @@ struct ConstraintSolver
// Memoized instantiations of type aliases. // Memoized instantiations of type aliases.
DenseHashMap<InstantiationSignature, TypeId, HashInstantiationSignature> instantiatedAliases{{}}; DenseHashMap<InstantiationSignature, TypeId, HashInstantiationSignature> instantiatedAliases{{}};
// Recorded errors that take place within the solver.
ErrorVec errors;
ConstraintSolverLogger logger; ConstraintSolverLogger logger;
explicit ConstraintSolver(TypeArena* arena, NotNull<Scope> rootScope); explicit ConstraintSolver(TypeArena* arena, NotNull<Scope> rootScope);
@ -115,7 +118,7 @@ struct ConstraintSolver
* @param subType the sub-type to unify. * @param subType the sub-type to unify.
* @param superType the super-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 * 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 subPack the sub-type pack to unify.
* @param superPack the super-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. /** Pushes a new solver constraint to the solver.
* @param cv the body of the constraint. * @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: private:
/** /**
* Marks a constraint as being blocked on a type or type pack. The constraint * 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); LoadDefinitionFileResult loadDefinitionFile(std::string_view source, const std::string& packageName);
NotNull<Scope> getGlobalScope(); ScopePtr getGlobalScope();
private: private:
ModulePtr check(const SourceModule& sourceModule, Mode mode, const ScopePtr& environmentScope); ModulePtr check(const SourceModule& sourceModule, Mode mode, const ScopePtr& environmentScope);

View file

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

View file

@ -77,6 +77,8 @@ struct Module
DenseHashMap<const AstExpr*, TypeId> astOverloadResolvedTypes{nullptr}; DenseHashMap<const AstExpr*, TypeId> astOverloadResolvedTypes{nullptr};
DenseHashMap<const AstType*, TypeId> astResolvedTypes{nullptr}; DenseHashMap<const AstType*, TypeId> astResolvedTypes{nullptr};
DenseHashMap<const AstTypePack*, TypePackId> astResolvedTypePacks{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; std::unordered_map<Name, TypeId> declaredGlobals;
ErrorVec errors; 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 // 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/Module.h"
#include "Luau/NotNull.h"
#include "Luau/TypeVar.h"
#include <memory>
namespace Luau namespace Luau
{ {
struct InternalErrorReporter; struct InternalErrorReporter;
struct Module;
struct Scope;
bool isSubtype(TypeId subTy, TypeId superTy, InternalErrorReporter& ice); using ModulePtr = std::shared_ptr<Module>;
bool isSubtype(TypePackId subTy, TypePackId superTy, InternalErrorReporter& ice);
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<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); std::pair<TypePackId, bool> normalize(TypePackId ty, const ModulePtr& module, InternalErrorReporter& ice);
} // namespace Luau } // namespace Luau

View file

@ -36,8 +36,6 @@ struct Scope
// All the children of this scope. // All the children of this scope.
std::vector<NotNull<Scope>> children; std::vector<NotNull<Scope>> children;
std::unordered_map<Symbol, Binding> bindings; std::unordered_map<Symbol, Binding> bindings;
std::unordered_map<Name, TypeFun> typeBindings;
std::unordered_map<Name, TypePackId> typePackBindings;
TypePackId returnType; TypePackId returnType;
std::optional<TypePackId> varargPack; std::optional<TypePackId> varargPack;
// All constraints belonging to this scope. // All constraints belonging to this scope.
@ -52,8 +50,6 @@ struct Scope
std::unordered_map<Name, std::unordered_map<Name, TypeFun>> importedTypeBindings; std::unordered_map<Name, std::unordered_map<Name, TypeFun>> importedTypeBindings;
std::optional<TypeId> lookup(Symbol sym); 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> lookupType(const Name& name);
std::optional<TypeFun> lookupImportedType(const Name& moduleAlias, 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 // This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#pragma once #pragma once
#include "Luau/Anyification.h"
#include "Luau/Predicate.h" #include "Luau/Predicate.h"
#include "Luau/Error.h" #include "Luau/Error.h"
#include "Luau/Module.h" #include "Luau/Module.h"
@ -16,6 +17,8 @@
#include <unordered_map> #include <unordered_map>
#include <unordered_set> #include <unordered_set>
LUAU_FASTFLAG(LuauClassTypeVarsInSubstitution)
namespace Luau namespace Luau
{ {
@ -34,37 +37,6 @@ const AstStat* getFallthrough(const AstStat* node);
struct UnifierOptions; struct UnifierOptions;
struct Unifier; 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 struct GenericTypeDefinitions
{ {
std::vector<GenericTypeDefinition> genericTypes; std::vector<GenericTypeDefinition> genericTypes;
@ -192,32 +164,32 @@ struct TypeChecker
/** Attempt to unify the types. /** Attempt to unify the types.
* Treat any failures as type errors in the final typecheck report. * 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 ScopePtr& scope, const Location& location);
bool unify(TypeId subTy, TypeId superTy, const Location& location, const UnifierOptions& options); bool unify(TypeId subTy, TypeId superTy, const ScopePtr& scope, const Location& location, const UnifierOptions& options);
bool unify(TypePackId subTy, TypePackId superTy, const Location& location, CountMismatch::Context ctx = CountMismatch::Context::Arg); bool unify(TypePackId subTy, TypePackId superTy, const ScopePtr& scope, const Location& location, CountMismatch::Context ctx = CountMismatch::Context::Arg);
/** Attempt to unify the types. /** Attempt to unify the types.
* If this fails, and the subTy type can be instantiated, do so and try unification again. * 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); bool unifyWithInstantiationIfNeeded(TypeId subTy, TypeId superTy, const ScopePtr& scope, const Location& location);
void unifyWithInstantiationIfNeeded(const ScopePtr& scope, TypeId subTy, TypeId superTy, Unifier& state); void unifyWithInstantiationIfNeeded(TypeId subTy, TypeId superTy, const ScopePtr& scope, Unifier& state);
/** Attempt to unify. /** Attempt to unify.
* If there are errors, undo everything and return the errors. * If there are errors, undo everything and return the errors.
* If there are no errors, commit and return an empty error vector. * If there are no errors, commit and return an empty error vector.
*/ */
template<typename Id> template<typename Id>
ErrorVec tryUnify_(Id subTy, Id superTy, const Location& location); ErrorVec tryUnify_(Id subTy, Id superTy, const ScopePtr& scope, const Location& location);
ErrorVec tryUnify(TypeId subTy, TypeId superTy, const Location& location); ErrorVec tryUnify(TypeId subTy, TypeId superTy, const ScopePtr& scope, const Location& location);
ErrorVec tryUnify(TypePackId subTy, TypePackId superTy, 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. // Test whether the two type vars unify. Never commits the result.
template<typename Id> template<typename Id>
ErrorVec canUnify_(Id subTy, Id superTy, const Location& location); ErrorVec canUnify_(Id subTy, Id superTy, const ScopePtr& scope, const Location& location);
ErrorVec canUnify(TypeId subTy, TypeId superTy, const Location& location); ErrorVec canUnify(TypeId subTy, TypeId superTy, const ScopePtr& scope, const Location& location);
ErrorVec canUnify(TypePackId subTy, TypePackId superTy, 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> findMetatableEntry(TypeId type, std::string entry, const Location& location, bool addErrors);
std::optional<TypeId> findTablePropertyRespectingMeta(TypeId lhsType, Name name, const Location& location, bool addErrors); std::optional<TypeId> findTablePropertyRespectingMeta(TypeId lhsType, Name name, const Location& location, bool addErrors);
@ -286,7 +258,7 @@ private:
void reportErrorCodeTooComplex(const Location& location); void reportErrorCodeTooComplex(const Location& location);
private: 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. // 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); std::pair<std::optional<TypeId>, bool> pickTypesFromSense(TypeId type, bool sense);
private: 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 // ex
// TypeId id = addType(FreeTypeVar()); // TypeId id = addType(FreeTypeVar());

View file

@ -13,7 +13,10 @@ namespace Luau
using ScopePtr = std::shared_ptr<struct Scope>; using ScopePtr = std::shared_ptr<struct Scope>;
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);
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);
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 } // namespace Luau

View file

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

View file

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

View file

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

View file

@ -69,12 +69,14 @@ struct GenericTypeVarVisitor
using Set = S; using Set = S;
Set seen; Set seen;
bool skipBoundTypes = false;
int recursionCounter = 0; int recursionCounter = 0;
GenericTypeVarVisitor() = default; GenericTypeVarVisitor() = default;
explicit GenericTypeVarVisitor(Set seen) explicit GenericTypeVarVisitor(Set seen, bool skipBoundTypes = false)
: seen(std::move(seen)) : seen(std::move(seen))
, skipBoundTypes(skipBoundTypes)
{ {
} }
@ -199,7 +201,9 @@ struct GenericTypeVarVisitor
if (auto btv = get<BoundTypeVar>(ty)) if (auto btv = get<BoundTypeVar>(ty))
{ {
if (visit(ty, *btv)) if (skipBoundTypes)
traverse(btv->boundTo);
else if (visit(ty, *btv))
traverse(btv->boundTo); traverse(btv->boundTo);
} }
else if (auto ftv = get<FreeTypeVar>(ty)) else if (auto ftv = get<FreeTypeVar>(ty))
@ -229,7 +233,11 @@ struct GenericTypeVarVisitor
else if (auto ttv = get<TableTypeVar>(ty)) else if (auto ttv = get<TableTypeVar>(ty))
{ {
// Some visitors want to see bound tables, that's why we traverse the original type // 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) if (ttv->boundTo)
{ {
@ -394,13 +402,17 @@ struct GenericTypeVarVisitor
*/ */
struct TypeVarVisitor : GenericTypeVarVisitor<std::unordered_set<void*>> 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. /// 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*>> struct TypeVarOnceVisitor : GenericTypeVarVisitor<DenseHashSet<void*>>
{ {
TypeVarOnceVisitor() explicit TypeVarOnceVisitor(bool skipBoundTypes = false)
: GenericTypeVarVisitor{DenseHashSet<void*>{nullptr}} : 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" #include "Luau/ApplyTypeFunction.h"
LUAU_FASTFLAG(LuauClassTypeVarsInSubstitution)
namespace Luau namespace Luau
{ {
@ -31,6 +33,8 @@ bool ApplyTypeFunction::ignoreChildren(TypeId ty)
{ {
if (get<GenericTypeVar>(ty)) if (get<GenericTypeVar>(ty))
return true; return true;
else if (FFlag::LuauClassTypeVarsInSubstitution && get<ClassTypeVar>(ty))
return true;
else else
return false; return false;
} }

View file

@ -14,6 +14,8 @@
LUAU_FASTFLAG(LuauSelfCallAutocompleteFix3) LUAU_FASTFLAG(LuauSelfCallAutocompleteFix3)
LUAU_FASTFLAGVARIABLE(LuauAutocompleteFixGlobalOrder, false)
static const std::unordered_set<std::string> kStatementStartingKeywords = { static const std::unordered_set<std::string> kStatementStartingKeywords = {
"while", "if", "local", "repeat", "function", "do", "for", "return", "break", "continue", "type", "export"}; "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; 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; InternalErrorReporter iceReporter;
UnifierSharedState unifierState(&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(); return unifier.canUnify(subTy, superTy).empty();
} }
@ -148,12 +150,14 @@ static TypeCorrectKind checkTypeCorrectKind(const Module& module, TypeArena* typ
{ {
ty = follow(ty); 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); LUAU_ASSERT(!FFlag::LuauSelfCallAutocompleteFix3);
InternalErrorReporter iceReporter; InternalErrorReporter iceReporter;
UnifierSharedState unifierState(&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); unifier.tryUnify(subTy, superTy);
bool ok = unifier.errors.empty(); bool ok = unifier.errors.empty();
@ -167,11 +171,11 @@ static TypeCorrectKind checkTypeCorrectKind(const Module& module, TypeArena* typ
TypeId expectedType = follow(*typeAtPosition); 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 (FFlag::LuauSelfCallAutocompleteFix3)
{ {
if (std::optional<TypeId> firstRetTy = first(ftv->retTypes)) if (std::optional<TypeId> firstRetTy = first(ftv->retTypes))
return checkTypeMatch(typeArena, *firstRetTy, expectedType); return checkTypeMatch(*firstRetTy, expectedType, moduleScope, typeArena);
return false; return false;
} }
@ -210,7 +214,7 @@ static TypeCorrectKind checkTypeCorrectKind(const Module& module, TypeArena* typ
} }
if (FFlag::LuauSelfCallAutocompleteFix3) 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 else
return canUnify(ty, expectedType) ? TypeCorrectKind::Correct : TypeCorrectKind::None; return canUnify(ty, expectedType) ? TypeCorrectKind::Correct : TypeCorrectKind::None;
} }
@ -268,7 +272,7 @@ static void autocompleteProps(const Module& module, TypeArena* typeArena, TypeId
return colonIndex; return colonIndex;
} }
}; };
auto isWrongIndexer = [typeArena, rootTy, indexType](Luau::TypeId type) { auto isWrongIndexer = [typeArena, &module, rootTy, indexType](Luau::TypeId type) {
LUAU_ASSERT(FFlag::LuauSelfCallAutocompleteFix3); LUAU_ASSERT(FFlag::LuauSelfCallAutocompleteFix3);
if (indexType == PropIndexType::Key) if (indexType == PropIndexType::Key)
@ -276,7 +280,7 @@ static void autocompleteProps(const Module& module, TypeArena* typeArena, TypeId
bool calledWithSelf = indexType == PropIndexType::Colon; 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 // Strong match with definition is a success
if (calledWithSelf == ftv->hasSelf) if (calledWithSelf == ftv->hasSelf)
return true; 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 // 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 (std::optional<TypeId> firstArgTy = first(ftv->argTypes))
{ {
if (checkTypeMatch(typeArena, rootTy, *firstArgTy)) if (checkTypeMatch(rootTy, *firstArgTy, NotNull{module.getModuleScope().get()}, typeArena))
return calledWithSelf; return calledWithSelf;
} }
@ -1073,10 +1077,21 @@ T* extractStat(const std::vector<AstNode*>& ancestry)
return nullptr; 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. if (FFlag::LuauAutocompleteFixGlobalOrder)
return binding.location == Location() || binding.location.end < pos; {
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( static AutocompleteEntryMap autocompleteStatement(
@ -1097,7 +1112,7 @@ static AutocompleteEntryMap autocompleteStatement(
{ {
for (const auto& [name, binding] : scope->bindings) for (const auto& [name, binding] : scope->bindings)
{ {
if (!isBindingLegalAtCurrentPosition(binding, position)) if (!isBindingLegalAtCurrentPosition(name, binding, position))
continue; continue;
std::string n = toString(name); 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) const std::vector<AstNode*>& ancestry, Position position, AutocompleteEntryMap& result)
{ {
LUAU_ASSERT(!ancestry.empty()); LUAU_ASSERT(!ancestry.empty());
@ -1213,9 +1228,9 @@ static void autocompleteExpression(const SourceModule& sourceModule, const Modul
autocompleteProps(module, typeArena, *it, PropIndexType::Point, ancestry, result); autocompleteProps(module, typeArena, *it, PropIndexType::Point, ancestry, result);
} }
else if (autocompleteIfElseExpression(node, ancestry, position, result)) else if (autocompleteIfElseExpression(node, ancestry, position, result))
return; return AutocompleteContext::Keyword;
else if (node->is<AstExprFunction>()) else if (node->is<AstExprFunction>())
return; return AutocompleteContext::Unknown;
else else
{ {
// This is inefficient. :( // This is inefficient. :(
@ -1225,7 +1240,7 @@ static void autocompleteExpression(const SourceModule& sourceModule, const Modul
{ {
for (const auto& [name, binding] : scope->bindings) for (const auto& [name, binding] : scope->bindings)
{ {
if (!isBindingLegalAtCurrentPosition(binding, position)) if (!isBindingLegalAtCurrentPosition(name, binding, position))
continue; continue;
if (isBeingDefined(ancestry, name)) if (isBeingDefined(ancestry, name))
@ -1260,14 +1275,16 @@ static void autocompleteExpression(const SourceModule& sourceModule, const Modul
if (auto ty = findExpectedTypeAt(module, node, position)) if (auto ty = findExpectedTypeAt(module, node, position))
autocompleteStringSingleton(*ty, true, result); 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) TypeArena* typeArena, const std::vector<AstNode*>& ancestry, Position position)
{ {
AutocompleteEntryMap result; AutocompleteEntryMap result;
autocompleteExpression(sourceModule, module, typeChecker, typeArena, ancestry, position, result); AutocompleteContext context = autocompleteExpression(sourceModule, module, typeChecker, typeArena, ancestry, position, result);
return result; return {result, ancestry, context};
} }
static std::optional<const ClassTypeVar*> getMethodContainingClass(const ModulePtr& module, AstExpr* funcExpr) 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)) if (!FFlag::LuauSelfCallAutocompleteFix3 && isString(ty))
return { 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 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>()) else if (auto typeReference = node->as<AstTypeReference>())
{ {
if (typeReference->prefix) if (typeReference->prefix)
return {autocompleteModuleTypes(*module, position, typeReference->prefix->value), ancestry}; return {autocompleteModuleTypes(*module, position, typeReference->prefix->value), ancestry, AutocompleteContext::Type};
else else
return {autocompleteTypeNames(*module, position, ancestry), ancestry}; return {autocompleteTypeNames(*module, position, ancestry), ancestry, AutocompleteContext::Type};
} }
else if (node->is<AstTypeError>()) 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>()) else if (AstStatLocal* statLocal = node->as<AstStatLocal>())
{ {
if (statLocal->vars.size == 1 && (!statLocal->equalsSignLocation || position < statLocal->equalsSignLocation->begin)) 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) 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 else
return {}; return {};
} }
@ -1436,16 +1453,16 @@ static AutocompleteResult autocomplete(const SourceModule& sourceModule, const M
if (!statFor->hasDo || position < statFor->doLocation.begin) if (!statFor->hasDo || position < statFor->doLocation.begin)
{ {
if (!statFor->from->is<AstExprError>() && !statFor->to->is<AstExprError>() && (!statFor->step || !statFor->step->is<AstExprError>())) 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) || if (statFor->from->location.containsClosed(position) || statFor->to->location.containsClosed(position) ||
(statFor->step && statFor->step->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 {};
} }
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))) 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 {};
} }
return {{{"in", AutocompleteEntry{AutocompleteEntryKind::Keyword}}}, ancestry}; return {{{"in", AutocompleteEntry{AutocompleteEntryKind::Keyword}}}, ancestry, AutocompleteContext::Keyword};
} }
if (!statForIn->hasDo || position <= statForIn->doLocation.begin) 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]; AstExpr* lastExpr = statForIn->values.data[statForIn->values.size - 1];
if (lastExpr->location.containsClosed(position)) 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) 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 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. // 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" // ex "for f in f do"
if (!statForIn->hasDo) 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) else if (AstStatWhile* statWhile = parent->as<AstStatWhile>(); node->is<AstStatBlock>() && statWhile)
{ {
if (!statWhile->hasDo && !statWhile->condition->is<AstStatError>() && position > statWhile->condition->location.end) 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) 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) 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) 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()) else if (AstStatIf* statIf = node->as<AstStatIf>(); statIf && !statIf->elseLocation.has_value())
{ {
return { 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>()) else if (AstStatIf* statIf = parent->as<AstStatIf>(); statIf && node->is<AstStatBlock>())
{ {
if (statIf->condition->is<AstExprError>()) 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)) 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); else if (AstStatIf* statIf = extractStat<AstStatIf>(ancestry);
statIf && (!statIf->thenLocation || statIf->thenLocation->containsClosed(position))) 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>()) 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) 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>())) else if (AstExprTable* exprTable = parent->as<AstExprTable>(); exprTable && (node->is<AstExprGlobal>() || node->is<AstExprConstantString>()))
{ {
for (const auto& [kind, key, value] : exprTable->items) for (const auto& [kind, key, value] : exprTable->items)
@ -1547,7 +1564,7 @@ static AutocompleteResult autocomplete(const SourceModule& sourceModule, const M
if (!key) if (!key)
autocompleteExpression(sourceModule, *module, typeChecker, typeArena, ancestry, position, result); autocompleteExpression(sourceModule, *module, typeChecker, typeArena, ancestry, position, result);
return {result, ancestry}; return {result, ancestry, AutocompleteContext::Property};
} }
break; break;
@ -1555,11 +1572,11 @@ static AutocompleteResult autocomplete(const SourceModule& sourceModule, const M
} }
} }
else if (isIdentifier(node) && (parent->is<AstStatExpr>() || parent->is<AstStatError>())) 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)) if (std::optional<AutocompleteEntryMap> ret = autocompleteStringParams(sourceModule, module, ancestry, position, callback))
{ {
return {*ret, ancestry}; return {*ret, ancestry, AutocompleteContext::String};
} }
else if (node->is<AstExprConstantString>()) 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>()) if (node->is<AstExprConstantNumber>())
@ -1594,9 +1611,9 @@ static AutocompleteResult autocomplete(const SourceModule& sourceModule, const M
} }
if (node->asExpr()) if (node->asExpr())
return {autocompleteExpression(sourceModule, *module, typeChecker, typeArena, ancestry, position), ancestry}; return autocompleteExpression(sourceModule, *module, typeChecker, typeArena, ancestry, position);
else if (node->asStat()) else if (node->asStat())
return {autocompleteStatement(sourceModule, *module, ancestry, position), ancestry}; return {autocompleteStatement(sourceModule, *module, ancestry, position), ancestry, AutocompleteContext::Statement};
return {}; return {};
} }

View file

@ -7,6 +7,7 @@
#include "Luau/Unifiable.h" #include "Luau/Unifiable.h"
LUAU_FASTFLAG(DebugLuauCopyBeforeNormalizing) LUAU_FASTFLAG(DebugLuauCopyBeforeNormalizing)
LUAU_FASTFLAG(LuauClonePublicInterfaceLess)
LUAU_FASTINTVARIABLE(LuauTypeCloneRecursionLimit, 300) LUAU_FASTINTVARIABLE(LuauTypeCloneRecursionLimit, 300)
@ -445,7 +446,7 @@ TypeFun clone(const TypeFun& typeFun, TypeArena& dest, CloneState& cloneState)
return result; 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); 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}; PendingExpansionTypeVar clone{petv->fn, petv->typeArguments, petv->packArguments};
result = dest.addType(std::move(clone)); 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 else
return result; return result;

View file

@ -5,8 +5,9 @@
namespace Luau namespace Luau
{ {
Constraint::Constraint(ConstraintV&& c) Constraint::Constraint(ConstraintV&& c, NotNull<Scope> scope)
: c(std::move(c)) : 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 // 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/ConstraintGraphBuilder.h"
#include "Luau/Ast.h"
#include "Luau/Common.h"
#include "Luau/Constraint.h"
#include "Luau/RecursionCounter.h" #include "Luau/RecursionCounter.h"
#include "Luau/ToString.h" #include "Luau/ToString.h"
@ -14,8 +17,9 @@ namespace Luau
const AstStat* getFallthrough(const AstStat* node); // TypeInfer.cpp const AstStat* getFallthrough(const AstStat* node); // TypeInfer.cpp
ConstraintGraphBuilder::ConstraintGraphBuilder( 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) : moduleName(moduleName)
, module(module)
, singletonTypes(getSingletonTypes()) , singletonTypes(getSingletonTypes())
, arena(arena) , arena(arena)
, rootScope(nullptr) , rootScope(nullptr)
@ -23,6 +27,7 @@ ConstraintGraphBuilder::ConstraintGraphBuilder(
, globalScope(globalScope) , globalScope(globalScope)
{ {
LUAU_ASSERT(arena); LUAU_ASSERT(arena);
LUAU_ASSERT(module);
} }
TypeId ConstraintGraphBuilder::freshType(const ScopePtr& scope) TypeId ConstraintGraphBuilder::freshType(const ScopePtr& scope)
@ -36,20 +41,22 @@ TypePackId ConstraintGraphBuilder::freshTypePack(const ScopePtr& scope)
return arena->addTypePack(TypePackVar{std::move(f)}); 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); auto scope = std::make_shared<Scope>(parent);
scopes.emplace_back(location, scope); scopes.emplace_back(node->location, scope);
scope->returnType = parent->returnType; 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; return scope;
} }
void ConstraintGraphBuilder::addConstraint(const ScopePtr& scope, ConstraintV cv) 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) 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(scopes.empty());
LUAU_ASSERT(rootScope == nullptr); LUAU_ASSERT(rootScope == nullptr);
ScopePtr scope = std::make_shared<Scope>(singletonTypes.anyTypePack); ScopePtr scope = std::make_shared<Scope>(globalScope);
rootScope = scope.get(); rootScope = scope.get();
scopes.emplace_back(block->location, scope); scopes.emplace_back(block->location, scope);
module->astScopes[block] = NotNull{scope.get()};
rootScope->returnType = freshTypePack(scope); rootScope->returnType = freshTypePack(scope);
prepopulateGlobalScope(scope, block); prepopulateGlobalScope(scope, block);
// TODO: We should share the global scope. // TODO: We should share the global scope.
rootScope->typeBindings["nil"] = TypeFun{singletonTypes.nilType}; rootScope->privateTypeBindings["nil"] = TypeFun{singletonTypes.nilType};
rootScope->typeBindings["number"] = TypeFun{singletonTypes.numberType}; rootScope->privateTypeBindings["number"] = TypeFun{singletonTypes.numberType};
rootScope->typeBindings["string"] = TypeFun{singletonTypes.stringType}; rootScope->privateTypeBindings["string"] = TypeFun{singletonTypes.stringType};
rootScope->typeBindings["boolean"] = TypeFun{singletonTypes.booleanType}; rootScope->privateTypeBindings["boolean"] = TypeFun{singletonTypes.booleanType};
rootScope->typeBindings["thread"] = TypeFun{singletonTypes.threadType}; rootScope->privateTypeBindings["thread"] = TypeFun{singletonTypes.threadType};
visitBlockWithoutChildScope(scope, block); visitBlockWithoutChildScope(scope, block);
} }
@ -99,7 +107,7 @@ void ConstraintGraphBuilder::visitBlockWithoutChildScope(const ScopePtr& scope,
{ {
if (auto alias = stat->as<AstStatTypeAlias>()) 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); auto it = aliasDefinitionLocations.find(alias->name.value);
LUAU_ASSERT(it != aliasDefinitionLocations.end()); LUAU_ASSERT(it != aliasDefinitionLocations.end());
@ -112,7 +120,7 @@ void ConstraintGraphBuilder::visitBlockWithoutChildScope(const ScopePtr& scope,
ScopePtr defnScope = scope; ScopePtr defnScope = scope;
if (hasGenerics) if (hasGenerics)
{ {
defnScope = childScope(alias->location, scope); defnScope = childScope(alias, scope);
} }
TypeId initialType = freshType(scope); TypeId initialType = freshType(scope);
@ -121,16 +129,16 @@ void ConstraintGraphBuilder::visitBlockWithoutChildScope(const ScopePtr& scope,
for (const auto& [name, gen] : createGenerics(defnScope, alias->generics)) for (const auto& [name, gen] : createGenerics(defnScope, alias->generics))
{ {
initialFun.typeParams.push_back(gen); 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)) for (const auto& [name, genPack] : createGenericPacks(defnScope, alias->genericPacks))
{ {
initialFun.typePackParams.push_back(genPack); 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; astTypeAliasDefiningScopes[alias] = defnScope;
aliasDefinitionLocations[alias->name.value] = alias->location; aliasDefinitionLocations[alias->name.value] = alias->location;
} }
@ -150,6 +158,10 @@ void ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStat* stat)
visit(scope, s); visit(scope, s);
else if (auto s = stat->as<AstStatFor>()) else if (auto s = stat->as<AstStatFor>())
visit(scope, s); 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>()) else if (auto f = stat->as<AstStatFunction>())
visit(scope, f); visit(scope, f);
else if (auto f = stat->as<AstStatLocalFunction>()) else if (auto f = stat->as<AstStatLocalFunction>())
@ -158,6 +170,8 @@ void ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStat* stat)
visit(scope, r); visit(scope, r);
else if (auto a = stat->as<AstStatAssign>()) else if (auto a = stat->as<AstStatAssign>())
visit(scope, a); visit(scope, a);
else if (auto a = stat->as<AstStatCompoundAssign>())
visit(scope, a);
else if (auto e = stat->as<AstStatExpr>()) else if (auto e = stat->as<AstStatExpr>())
checkPack(scope, e->expr); checkPack(scope, e->expr);
else if (auto i = stat->as<AstStatIf>()) else if (auto i = stat->as<AstStatIf>())
@ -236,12 +250,32 @@ void ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatFor* for_)
checkNumber(for_->to); checkNumber(for_->to);
checkNumber(for_->step); checkNumber(for_->step);
ScopePtr forScope = childScope(for_->location, scope); ScopePtr forScope = childScope(for_, scope);
forScope->bindings[for_->var] = Binding{singletonTypes.numberType, for_->var->location}; forScope->bindings[for_->var] = Binding{singletonTypes.numberType, for_->var->location};
visit(forScope, for_->body); 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) void addConstraints(Constraint* constraint, NotNull<Scope> scope)
{ {
scope->constraints.reserve(scope->constraints.size() + scope->constraints.size()); 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); checkFunctionBody(sig.bodyScope, function->func);
std::unique_ptr<Constraint> c{ NotNull<Scope> constraintScope{sig.signatureScope ? sig.signatureScope.get() : sig.bodyScope.get()};
new Constraint{GeneralizationConstraint{functionType, sig.signature, 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())); addConstraints(c.get(), NotNull(sig.bodyScope.get()));
addConstraint(scope, std::move(c)); addConstraint(scope, std::move(c));
@ -342,8 +376,8 @@ void ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatFunction* funct
checkFunctionBody(sig.bodyScope, function->func); checkFunctionBody(sig.bodyScope, function->func);
std::unique_ptr<Constraint> c{ NotNull<Scope> constraintScope{sig.signatureScope ? sig.signatureScope.get() : sig.bodyScope.get()};
new Constraint{GeneralizationConstraint{functionType, sig.signature, 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())); addConstraints(c.get(), NotNull(sig.bodyScope.get()));
addConstraint(scope, std::move(c)); 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) void ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatBlock* block)
{ {
ScopePtr innerScope = childScope(block->location, scope); ScopePtr innerScope = childScope(block, scope);
visitBlockWithoutChildScope(innerScope, block); visitBlockWithoutChildScope(innerScope, block);
} }
@ -370,16 +404,30 @@ void ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatAssign* assign)
addConstraint(scope, PackSubtypeConstraint{valuePack, varPackId}); 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) void ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatIf* ifStatement)
{ {
check(scope, ifStatement->condition); check(scope, ifStatement->condition);
ScopePtr thenScope = childScope(ifStatement->thenbody->location, scope); ScopePtr thenScope = childScope(ifStatement->thenbody, scope);
visit(thenScope, ifStatement->thenbody); visit(thenScope, ifStatement->thenbody);
if (ifStatement->elsebody) if (ifStatement->elsebody)
{ {
ScopePtr elseScope = childScope(ifStatement->elsebody->location, scope); ScopePtr elseScope = childScope(ifStatement->elsebody, scope);
visit(elseScope, ifStatement->elsebody); visit(elseScope, ifStatement->elsebody);
} }
} }
@ -388,11 +436,11 @@ void ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatTypeAlias* alia
{ {
// TODO: Exported type aliases // 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); ScopePtr* defnIt = astTypeAliasDefiningScopes.find(alias);
// These will be undefined if the alias was a duplicate definition, in which // These will be undefined if the alias was a duplicate definition, in which
// case we just skip over it. // case we just skip over it.
if (bindingIt == scope->typeBindings.end() || defnIt == nullptr) if (bindingIt == scope->privateTypeBindings.end() || defnIt == nullptr)
{ {
return; return;
} }
@ -416,17 +464,152 @@ void ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatDeclareGlobal*
LUAU_ASSERT(global->type); LUAU_ASSERT(global->type);
TypeId globalTy = resolveType(scope, 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}; 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) 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) 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); result = check(scope, unary);
else if (auto binary = expr->as<AstExprBinary>()) else if (auto binary = expr->as<AstExprBinary>())
result = check(scope, binary); 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>()) else if (auto err = expr->as<AstExprError>())
{ {
// Open question: Should we traverse into this? // Open question: Should we traverse into this?
@ -668,6 +855,12 @@ TypeId ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprBinary* binar
addConstraint(scope, SubtypeConstraint{leftType, rightType}); addConstraint(scope, SubtypeConstraint{leftType, rightType});
return leftType; return leftType;
} }
case AstExprBinary::Add:
{
TypeId resultType = arena->addType(BlockedTypeVar{});
addConstraint(scope, BinaryConstraint{AstExprBinary::Add, leftType, rightType, resultType});
return resultType;
}
case AstExprBinary::Sub: case AstExprBinary::Sub:
{ {
TypeId resultType = arena->addType(BlockedTypeVar{}); TypeId resultType = arena->addType(BlockedTypeVar{});
@ -682,6 +875,30 @@ TypeId ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprBinary* binar
return nullptr; 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 ConstraintGraphBuilder::checkExprTable(const ScopePtr& scope, AstExprTable* expr)
{ {
TypeId ty = arena->addType(TableTypeVar{}); TypeId ty = arena->addType(TableTypeVar{});
@ -748,14 +965,14 @@ ConstraintGraphBuilder::FunctionSignature ConstraintGraphBuilder::checkFunctionS
// generics properly. // generics properly.
if (hasGenerics) if (hasGenerics)
{ {
signatureScope = childScope(fn->location, parent); signatureScope = childScope(fn, parent);
// We need to assign returnType before creating bodyScope so that the // We need to assign returnType before creating bodyScope so that the
// return type gets propogated to bodyScope. // return type gets propogated to bodyScope.
returnType = freshTypePack(signatureScope); returnType = freshTypePack(signatureScope);
signatureScope->returnType = returnType; 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, GenericTypeDefinition>> genericDefinitions = createGenerics(signatureScope, fn->generics);
std::vector<std::pair<Name, GenericTypePackDefinition>> genericPackDefinitions = createGenericPacks(signatureScope, fn->genericPacks); 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) for (const auto& [name, g] : genericDefinitions)
{ {
genericTypes.push_back(g.ty); genericTypes.push_back(g.ty);
signatureScope->typeBindings[name] = TypeFun{g.ty}; signatureScope->privateTypeBindings[name] = TypeFun{g.ty};
} }
for (const auto& [name, g] : genericPackDefinitions) for (const auto& [name, g] : genericPackDefinitions)
{ {
genericTypePacks.push_back(g.tp); genericTypePacks.push_back(g.tp);
signatureScope->typePackBindings[name] = g.tp; signatureScope->privateTypePackBindings[name] = g.tp;
} }
} }
else else
{ {
bodyScope = childScope(fn->body->location, parent); bodyScope = childScope(fn->body, parent);
returnType = freshTypePack(bodyScope); returnType = freshTypePack(bodyScope);
bodyScope->returnType = returnType; bodyScope->returnType = returnType;
@ -851,7 +1068,7 @@ TypeId ConstraintGraphBuilder::resolveType(const ScopePtr& scope, AstType* ty, b
// TODO: Support imported types w/ require tracing. // TODO: Support imported types w/ require tracing.
LUAU_ASSERT(!ref->prefix); 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()) if (alias.has_value())
{ {
@ -941,7 +1158,7 @@ TypeId ConstraintGraphBuilder::resolveType(const ScopePtr& scope, AstType* ty, b
// for the generic bindings to live on. // for the generic bindings to live on.
if (hasGenerics) 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, GenericTypeDefinition>> genericDefinitions = createGenerics(signatureScope, fn->generics);
std::vector<std::pair<Name, GenericTypePackDefinition>> genericPackDefinitions = createGenericPacks(signatureScope, fn->genericPacks); 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) for (const auto& [name, g] : genericDefinitions)
{ {
genericTypes.push_back(g.ty); genericTypes.push_back(g.ty);
signatureScope->typeBindings[name] = TypeFun{g.ty}; signatureScope->privateTypeBindings[name] = TypeFun{g.ty};
} }
for (const auto& [name, g] : genericPackDefinitions) for (const auto& [name, g] : genericPackDefinitions)
{ {
genericTypePacks.push_back(g.tp); genericTypePacks.push_back(g.tp);
signatureScope->typePackBindings[name] = g.tp; signatureScope->privateTypePackBindings[name] = g.tp;
} }
} }
else else
@ -1059,7 +1276,7 @@ TypePackId ConstraintGraphBuilder::resolveTypePack(const ScopePtr& scope, AstTyp
} }
else if (auto gen = tp->as<AstTypePackGeneric>()) 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; result = *lookup;
} }

View file

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

View file

@ -818,27 +818,30 @@ const SourceModule* Frontend::getSourceModule(const ModuleName& moduleName) cons
return const_cast<Frontend*>(this)->getSourceModule(moduleName); return const_cast<Frontend*>(this)->getSourceModule(moduleName);
} }
NotNull<Scope> Frontend::getGlobalScope() ScopePtr Frontend::getGlobalScope()
{ {
if (!globalScope) if (!globalScope)
{ {
globalScope = typeChecker.globalScope; globalScope = typeChecker.globalScope;
} }
return NotNull(globalScope.get()); return globalScope;
} }
ModulePtr Frontend::check(const SourceModule& sourceModule, Mode mode, const ScopePtr& environmentScope) ModulePtr Frontend::check(const SourceModule& sourceModule, Mode mode, const ScopePtr& environmentScope)
{ {
ModulePtr result = std::make_shared<Module>(); 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); cgb.visit(sourceModule.root);
result->errors = std::move(cgb.errors); result->errors = std::move(cgb.errors);
ConstraintSolver cs{&result->internalTypes, NotNull(cgb.rootScope)}; ConstraintSolver cs{&result->internalTypes, NotNull(cgb.rootScope)};
cs.run(); cs.run();
for (TypeError& e : cs.errors)
result->errors.emplace_back(std::move(e));
result->scopes = std::move(cgb.scopes); result->scopes = std::move(cgb.scopes);
result->astTypes = std::move(cgb.astTypes); result->astTypes = std::move(cgb.astTypes);
result->astTypePacks = std::move(cgb.astTypePacks); 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. // 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); return (!generics.empty() || !genericPacks.empty()) && (ftv->generics == generics) && (ftv->genericPacks == genericPacks);
} }
else if (FFlag::LuauClassTypeVarsInSubstitution && get<ClassTypeVar>(ty))
return true;
else else
{ {
return false; return false;

View file

@ -14,6 +14,7 @@
LUAU_FASTINTVARIABLE(LuauSuggestionDistance, 4) LUAU_FASTINTVARIABLE(LuauSuggestionDistance, 4)
LUAU_FASTFLAGVARIABLE(LuauLintGlobalNeverReadBeforeWritten, false) LUAU_FASTFLAGVARIABLE(LuauLintGlobalNeverReadBeforeWritten, false)
LUAU_FASTFLAGVARIABLE(LuauLintComparisonPrecedence, false)
namespace Luau namespace Luau
{ {
@ -49,6 +50,7 @@ static const char* kWarningNames[] = {
"MisleadingAndOr", "MisleadingAndOr",
"CommentDirective", "CommentDirective",
"IntegerParsing", "IntegerParsing",
"ComparisonPrecedence",
}; };
// clang-format on // 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) static void fillBuiltinGlobals(LintContext& context, const AstNameTable& names, const ScopePtr& env)
{ {
ScopePtr current = 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)) if (context.warningEnabled(LintWarning::Code_IntegerParsing))
LintIntegerParsing::process(context); LintIntegerParsing::process(context);
if (context.warningEnabled(LintWarning::Code_ComparisonPrecedence) && FFlag::LuauLintComparisonPrecedence)
LintComparisonPrecedence::process(context);
std::sort(context.result.begin(), context.result.end(), WarningComparator()); std::sort(context.result.begin(), context.result.end(), WarningComparator());
return context.result; return context.result;

View file

@ -17,6 +17,10 @@
LUAU_FASTFLAG(LuauLowerBoundsCalculation); LUAU_FASTFLAG(LuauLowerBoundsCalculation);
LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution); LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution);
LUAU_FASTFLAGVARIABLE(LuauForceExportSurfacesToBeNormal, false); LUAU_FASTFLAGVARIABLE(LuauForceExportSurfacesToBeNormal, false);
LUAU_FASTFLAGVARIABLE(LuauClonePublicInterfaceLess, false);
LUAU_FASTFLAG(LuauSubstitutionReentrant);
LUAU_FASTFLAG(LuauClassTypeVarsInSubstitution);
LUAU_FASTFLAG(LuauSubstitutionFixMissingFields);
namespace Luau 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() Module::~Module()
{ {
unfreeze(interfaceTypes); unfreeze(interfaceTypes);
@ -106,12 +222,21 @@ void Module::clonePublicInterface(InternalErrorReporter& ice)
std::unordered_map<Name, TypeFun>* exportedTypeBindings = std::unordered_map<Name, TypeFun>* exportedTypeBindings =
FFlag::DebugLuauDeferredConstraintResolution ? nullptr : &moduleScope->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; moduleScope->returnType = returnType;
if (varargPack) if (varargPack)
{ {
varargPack = clone(*varargPack, interfaceTypes, cloneState); if (FFlag::LuauClonePublicInterfaceLess)
varargPack = clonePublicInterface.cloneTypePack(*varargPack);
else
varargPack = clone(*varargPack, interfaceTypes, cloneState);
moduleScope->varargPack = varargPack; moduleScope->varargPack = varargPack;
} }
@ -119,12 +244,12 @@ void Module::clonePublicInterface(InternalErrorReporter& ice)
if (FFlag::LuauLowerBoundsCalculation) if (FFlag::LuauLowerBoundsCalculation)
{ {
normalize(returnType, interfaceTypes, ice); normalize(returnType, NotNull{this}, ice);
if (FFlag::LuauForceExportSurfacesToBeNormal) if (FFlag::LuauForceExportSurfacesToBeNormal)
forceNormal.traverse(returnType); forceNormal.traverse(returnType);
if (varargPack) if (varargPack)
{ {
normalize(*varargPack, interfaceTypes, ice); normalize(*varargPack, NotNull{this}, ice);
if (FFlag::LuauForceExportSurfacesToBeNormal) if (FFlag::LuauForceExportSurfacesToBeNormal)
forceNormal.traverse(*varargPack); forceNormal.traverse(*varargPack);
} }
@ -134,10 +259,13 @@ void Module::clonePublicInterface(InternalErrorReporter& ice)
{ {
for (auto& [name, tf] : *exportedTypeBindings) 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) 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 // 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. // 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) if (param.defaultValue)
{ {
normalize(*param.defaultValue, interfaceTypes, ice); normalize(*param.defaultValue, NotNull{this}, ice);
forceNormal.traverse(*param.defaultValue); forceNormal.traverse(*param.defaultValue);
} }
} }
@ -168,10 +296,13 @@ void Module::clonePublicInterface(InternalErrorReporter& ice)
for (auto& [name, ty] : declaredGlobals) 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) if (FFlag::LuauLowerBoundsCalculation)
{ {
normalize(ty, interfaceTypes, ice); normalize(ty, NotNull{this}, ice);
if (FFlag::LuauForceExportSurfacesToBeNormal) if (FFlag::LuauForceExportSurfacesToBeNormal)
forceNormal.traverse(ty); forceNormal.traverse(ty);

View file

@ -15,7 +15,6 @@ LUAU_FASTINTVARIABLE(LuauNormalizeIterationLimit, 1200);
LUAU_FASTFLAGVARIABLE(LuauNormalizeCombineTableFix, false); LUAU_FASTFLAGVARIABLE(LuauNormalizeCombineTableFix, false);
LUAU_FASTFLAGVARIABLE(LuauFixNormalizationOfCyclicUnions, false); LUAU_FASTFLAGVARIABLE(LuauFixNormalizationOfCyclicUnions, false);
LUAU_FASTFLAG(LuauUnknownAndNeverType) LUAU_FASTFLAG(LuauUnknownAndNeverType)
LUAU_FASTFLAG(LuauQuantifyConstrained)
namespace Luau namespace Luau
{ {
@ -55,11 +54,11 @@ struct Replacer
} // anonymous namespace } // anonymous namespace
bool isSubtype(TypeId subTy, TypeId superTy, InternalErrorReporter& ice) bool isSubtype(TypeId subTy, TypeId superTy, NotNull<Scope> scope, InternalErrorReporter& ice)
{ {
UnifierSharedState sharedState{&ice}; UnifierSharedState sharedState{&ice};
TypeArena arena; TypeArena arena;
Unifier u{&arena, Mode::Strict, Location{}, Covariant, sharedState}; Unifier u{&arena, Mode::Strict, scope, Location{}, Covariant, sharedState};
u.anyIsTop = true; u.anyIsTop = true;
u.tryUnify(subTy, superTy); u.tryUnify(subTy, superTy);
@ -67,11 +66,11 @@ bool isSubtype(TypeId subTy, TypeId superTy, InternalErrorReporter& ice)
return ok; return ok;
} }
bool isSubtype(TypePackId subPack, TypePackId superPack, InternalErrorReporter& ice) bool isSubtype(TypePackId subPack, TypePackId superPack, NotNull<Scope> scope, InternalErrorReporter& ice)
{ {
UnifierSharedState sharedState{&ice}; UnifierSharedState sharedState{&ice};
TypeArena arena; TypeArena arena;
Unifier u{&arena, Mode::Strict, Location{}, Covariant, sharedState}; Unifier u{&arena, Mode::Strict, scope, Location{}, Covariant, sharedState};
u.anyIsTop = true; u.anyIsTop = true;
u.tryUnify(subPack, superPack); u.tryUnify(subPack, superPack);
@ -134,13 +133,15 @@ struct Normalize final : TypeVarVisitor
{ {
using TypeVarVisitor::Set; using TypeVarVisitor::Set;
Normalize(TypeArena& arena, InternalErrorReporter& ice) Normalize(TypeArena& arena, NotNull<Scope> scope, InternalErrorReporter& ice)
: arena(arena) : arena(arena)
, scope(scope)
, ice(ice) , ice(ice)
{ {
} }
TypeArena& arena; TypeArena& arena;
NotNull<Scope> scope;
InternalErrorReporter& ice; InternalErrorReporter& ice;
int iterationLimit = 0; 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. // 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); 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; return !ty->normal;
} }
@ -214,22 +216,7 @@ struct Normalize final : TypeVarVisitor
traverse(part); traverse(part);
std::vector<TypeId> newParts = normalizeUnion(parts); std::vector<TypeId> newParts = normalizeUnion(parts);
ctv->parts = std::move(newParts);
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;
}
return false; 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. // 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))
{
if (ttv.state == TableState::Generic || ttv.state == TableState::Sealed || (ttv.state == TableState::Free && follow(ty)->normal))
asMutable(ty)->normal = normal;
}
else
asMutable(ty)->normal = normal; asMutable(ty)->normal = normal;
return false; return false;
@ -517,9 +499,9 @@ struct Normalize final : TypeVarVisitor
for (TypeId& part : result) for (TypeId& part : result)
{ {
if (isSubtype(ty, part, ice)) if (isSubtype(ty, part, scope, ice))
return; // no need to do anything 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 part = ty; // replace the less general type by the more general one
return; return;
@ -571,12 +553,12 @@ struct Normalize final : TypeVarVisitor
bool merged = false; bool merged = false;
for (TypeId& part : result->parts) for (TypeId& part : result->parts)
{ {
if (isSubtype(part, ty, ice)) if (isSubtype(part, ty, scope, ice))
{ {
merged = true; merged = true;
break; // no need to do anything break; // no need to do anything
} }
else if (isSubtype(ty, part, ice)) else if (isSubtype(ty, part, scope, ice))
{ {
merged = true; merged = true;
part = ty; // replace the less general type by the more general one 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) * @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; CloneState state;
if (FFlag::DebugLuauCopyBeforeNormalizing) if (FFlag::DebugLuauCopyBeforeNormalizing)
(void)clone(ty, arena, state); (void)clone(ty, arena, state);
Normalize n{arena, ice}; Normalize n{arena, scope, ice};
n.traverse(ty); n.traverse(ty);
return {ty, !n.limitExceeded}; 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. // 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 // The main wrinkle here is that we don't want clone() to copy a type if the source and dest
// arena are the same. // 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) 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) * @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; CloneState state;
if (FFlag::DebugLuauCopyBeforeNormalizing) if (FFlag::DebugLuauCopyBeforeNormalizing)
(void)clone(tp, arena, state); (void)clone(tp, arena, state);
Normalize n{arena, ice}; Normalize n{arena, scope, ice};
n.traverse(tp); n.traverse(tp);
return {tp, !n.limitExceeded}; 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) 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 } // namespace Luau

View file

@ -5,11 +5,12 @@
#include "Luau/Scope.h" #include "Luau/Scope.h"
#include "Luau/Substitution.h" #include "Luau/Substitution.h"
#include "Luau/TxnLog.h" #include "Luau/TxnLog.h"
#include "Luau/TypeVar.h"
#include "Luau/VisitTypeVar.h" #include "Luau/VisitTypeVar.h"
LUAU_FASTFLAG(DebugLuauSharedSelf) LUAU_FASTFLAG(DebugLuauSharedSelf)
LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution); LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution);
LUAU_FASTFLAGVARIABLE(LuauQuantifyConstrained, false) LUAU_FASTFLAG(LuauClassTypeVarsInSubstitution)
namespace Luau namespace Luau
{ {
@ -80,30 +81,25 @@ struct Quantifier final : TypeVarOnceVisitor
bool visit(TypeId ty, const ConstrainedTypeVar&) override bool visit(TypeId ty, const ConstrainedTypeVar&) override
{ {
if (FFlag::LuauQuantifyConstrained) ConstrainedTypeVar* ctv = getMutable<ConstrainedTypeVar>(ty);
{
ConstrainedTypeVar* ctv = getMutable<ConstrainedTypeVar>(ty);
seenMutableType = true; 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)};
if (FFlag::DebugLuauDeferredConstraintResolution ? !subsumes(scope, ctv->scope) : !level.subsumes(ctv->level))
return false; 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 else
return true; *asMutable(ty) = UnionTypeVar{std::move(opts)};
return false;
} }
bool visit(TypeId ty, const TableTypeVar&) override bool visit(TypeId ty, const TableTypeVar&) override
@ -117,12 +113,6 @@ struct Quantifier final : TypeVarOnceVisitor
if (ttv.state == TableState::Free) if (ttv.state == TableState::Free)
seenMutableType = true; 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 (FFlag::DebugLuauDeferredConstraintResolution ? !subsumes(scope, ttv.scope) : !level.subsumes(ttv.level))
{ {
if (ttv.state == TableState::Unsealed) if (ttv.state == TableState::Unsealed)
@ -297,6 +287,9 @@ struct PureQuantifier : Substitution
bool ignoreChildren(TypeId ty) override bool ignoreChildren(TypeId ty) override
{ {
if (FFlag::LuauClassTypeVarsInSubstitution && get<ClassTypeVar>(ty))
return true;
return ty->persistent; return ty->persistent;
} }
bool ignoreChildren(TypePackId ty) override 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 } // namespace Luau

View file

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

View file

@ -8,21 +8,59 @@
#include "Luau/Clone.h" #include "Luau/Clone.h"
#include "Luau/Instantiation.h" #include "Luau/Instantiation.h"
#include "Luau/Normalize.h" #include "Luau/Normalize.h"
#include "Luau/ToString.h"
#include "Luau/TxnLog.h" #include "Luau/TxnLog.h"
#include "Luau/TypeUtils.h" #include "Luau/TypeUtils.h"
#include "Luau/TypeVar.h"
#include "Luau/Unifier.h" #include "Luau/Unifier.h"
#include "Luau/ToString.h"
namespace Luau 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; const SourceModule* sourceModule;
Module* module; Module* module;
InternalErrorReporter ice; // FIXME accept a pointer from Frontend InternalErrorReporter ice; // FIXME accept a pointer from Frontend
SingletonTypes& singletonTypes; SingletonTypes& singletonTypes;
std::vector<NotNull<Scope>> stack;
TypeChecker2(const SourceModule* sourceModule, Module* module) TypeChecker2(const SourceModule* sourceModule, Module* module)
: sourceModule(sourceModule) : sourceModule(sourceModule)
, module(module) , 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) TypePackId lookupPack(AstExpr* expr)
{ {
@ -117,11 +161,128 @@ struct TypeChecker2 : public AstVisitor
return bestScope; 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) for (size_t i = 0; i < local->values.size; ++i)
{ {
AstExpr* value = local->values.data[i]; AstExpr* value = local->values.data[i];
visit(value);
if (i == local->values.size - 1) if (i == local->values.size - 1)
{ {
if (i < local->values.size) if (i < local->values.size)
@ -139,7 +300,7 @@ struct TypeChecker2 : public AstVisitor
if (var->annotation) if (var->annotation)
{ {
TypeId varType = lookupAnnotation(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); reportError(TypeMismatch{varType, *it}, value->location);
} }
@ -157,64 +318,244 @@ struct TypeChecker2 : public AstVisitor
if (var->annotation) if (var->annotation)
{ {
TypeId varType = lookupAnnotation(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); 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); size_t count = std::min(assign->vars.size, assign->values.size);
for (size_t i = 0; i < count; ++i) for (size_t i = 0; i < count; ++i)
{ {
AstExpr* lhs = assign->vars.data[i]; AstExpr* lhs = assign->vars.data[i];
visit(lhs);
TypeId lhsType = lookupType(lhs); TypeId lhsType = lookupType(lhs);
AstExpr* rhs = assign->values.data[i]; AstExpr* rhs = assign->values.data[i];
visit(rhs);
TypeId rhsType = lookupType(rhs); TypeId rhsType = lookupType(rhs);
if (!isSubtype(rhsType, lhsType, ice)) if (!isSubtype(rhsType, lhsType, stack.back(), ice))
{ {
reportError(TypeMismatch{lhsType, rhsType}, rhs->location); reportError(TypeMismatch{lhsType, rhsType}, rhs->location);
} }
} }
return true;
} }
bool visit(AstStatReturn* ret) override void visit(AstStatCompoundAssign* stat)
{ {
Scope* scope = findInnermostScope(ret->location); visit(stat->var);
TypePackId expectedRetType = scope->returnType; visit(stat->value);
}
TypeArena arena; void visit(AstStatFunction* stat)
TypePackId actualRetType = reconstructPack(ret->list, arena); {
visit(stat->name);
visit(stat->func);
}
UnifierSharedState sharedState{&ice}; void visit(AstStatLocalFunction* stat)
Unifier u{&arena, Mode::Strict, ret->location, Covariant, sharedState}; {
u.anyIsTop = true; visit(stat->func);
}
u.tryUnify(actualRetType, expectedRetType); void visit(const AstTypeList* typeList)
const bool ok = u.errors.empty() && u.log.empty(); {
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) if (el.defaultValue)
reportError(e); 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; TypeArena arena;
Instantiation instantiation{TxnLog::empty(), &arena, TypeLevel{}}; Instantiation instantiation{TxnLog::empty(), &arena, TypeLevel{}};
@ -224,7 +565,7 @@ struct TypeChecker2 : public AstVisitor
LUAU_ASSERT(functionType); LUAU_ASSERT(functionType);
TypePack args; TypePack args;
for (const auto& arg : call->args) for (AstExpr* arg : call->args)
{ {
TypeId argTy = module->astTypes[arg]; TypeId argTy = module->astTypes[arg];
LUAU_ASSERT(argTy); LUAU_ASSERT(argTy);
@ -234,7 +575,7 @@ struct TypeChecker2 : public AstVisitor
TypePackId argsTp = arena.addTypePack(args); TypePackId argsTp = arena.addTypePack(args);
FunctionTypeVar ftv{argsTp, expectedRetType}; FunctionTypeVar ftv{argsTp, expectedRetType};
TypeId expectedType = arena.addType(ftv); TypeId expectedType = arena.addType(ftv);
if (!isSubtype(expectedType, instantiatedFunctionType, ice)) if (!isSubtype(expectedType, instantiatedFunctionType, stack.back(), ice))
{ {
unfreeze(module->interfaceTypes); unfreeze(module->interfaceTypes);
CloneState cloneState; CloneState cloneState;
@ -242,12 +583,36 @@ struct TypeChecker2 : public AstVisitor
freeze(module->interfaceTypes); freeze(module->interfaceTypes);
reportError(TypeMismatch{expectedType, functionType}, call->location); 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); TypeId inferredFnTy = lookupType(fn);
const FunctionTypeVar* inferredFtv = get<FunctionTypeVar>(inferredFnTy); const FunctionTypeVar* inferredFtv = get<FunctionTypeVar>(inferredFnTy);
LUAU_ASSERT(inferredFtv); LUAU_ASSERT(inferredFtv);
@ -263,7 +628,7 @@ struct TypeChecker2 : public AstVisitor
TypeId inferredArgTy = *argIt; TypeId inferredArgTy = *argIt;
TypeId annotatedArgTy = lookupAnnotation(arg->annotation); TypeId annotatedArgTy = lookupAnnotation(arg->annotation);
if (!isSubtype(annotatedArgTy, inferredArgTy, ice)) if (!isSubtype(annotatedArgTy, inferredArgTy, stack.back(), ice))
{ {
reportError(TypeMismatch{annotatedArgTy, inferredArgTy}, arg->location); reportError(TypeMismatch{annotatedArgTy, inferredArgTy}, arg->location);
} }
@ -272,56 +637,64 @@ struct TypeChecker2 : public AstVisitor
++argIt; ++argIt;
} }
return true; visit(fn->body);
} }
bool visit(AstExprIndexName* indexName) override void visit(AstExprTable* expr)
{ {
TypeId leftType = lookupType(indexName->expr); // TODO!
TypeId resultType = lookupType(indexName); for (const AstExprTable::Item& item : expr->items)
// leftType must have a property called indexName->index
std::optional<TypeId> t = findTablePropertyRespectingMeta(module->errors, leftType, indexName->index.value, indexName->location);
if (t)
{ {
if (!isSubtype(resultType, *t, ice)) if (item.key)
{ visit(item.key);
reportError(TypeMismatch{resultType, *t}, indexName->location); 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); // TODO!
TypeId numberType = getSingletonTypes().numberType; visit(expr->expr);
if (!isSubtype(numberType, actualType, ice))
{
reportError(TypeMismatch{actualType, numberType}, number->location);
}
return true;
} }
bool visit(AstExprConstantString* string) override void visit(AstExprBinary* expr)
{ {
TypeId actualType = lookupType(string); // TODO!
TypeId stringType = getSingletonTypes().stringType; visit(expr->left);
visit(expr->right);
}
if (!isSubtype(stringType, actualType, ice)) void visit(AstExprTypeAssertion* expr)
{ {
reportError(TypeMismatch{actualType, stringType}, string->location); 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. /** 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!"); 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); Scope* scope = findInnermostScope(ty->location);
LUAU_ASSERT(scope); LUAU_ASSERT(scope);
// TODO: Imported types // 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()) if (alias.has_value())
{ {
@ -473,7 +865,7 @@ struct TypeChecker2 : public AstVisitor
} }
else else
{ {
if (scope->lookupTypePackBinding(ty->name.value)) if (scope->lookupPack(ty->name.value))
{ {
reportError( reportError(
SwappedGenericTypeParameter{ SwappedGenericTypeParameter{
@ -487,24 +879,84 @@ struct TypeChecker2 : public AstVisitor
reportError(UnknownSymbol{ty->name.value, UnknownSymbol::Context::Type}, ty->location); 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); Scope* scope = findInnermostScope(tp->location);
LUAU_ASSERT(scope); 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 (!alias.has_value())
{ {
if (scope->lookupTypeBinding(tp->genericName.value)) if (scope->lookupType(tp->genericName.value))
{ {
reportError( reportError(
SwappedGenericTypeParameter{ SwappedGenericTypeParameter{
@ -518,8 +970,6 @@ struct TypeChecker2 : public AstVisitor
reportError(UnknownSymbol{tp->genericName.value, UnknownSymbol::Context::Type}, tp->location); reportError(UnknownSymbol{tp->genericName.value, UnknownSymbol::Context::Type}, tp->location);
} }
} }
return true;
} }
void reportError(TypeErrorData&& data, const Location& location) void reportError(TypeErrorData&& data, const Location& location)
@ -531,13 +981,19 @@ struct TypeChecker2 : public AstVisitor
{ {
module->errors.emplace_back(std::move(e)); 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) void check(const SourceModule& sourceModule, Module* module)
{ {
TypeChecker2 typeChecker{&sourceModule, module}; TypeChecker2 typeChecker{&sourceModule, module};
sourceModule.root->visit(&typeChecker); typeChecker.visit(sourceModule.root);
} }
} // namespace Luau } // namespace Luau

View file

@ -33,16 +33,13 @@ LUAU_FASTINTVARIABLE(LuauVisitRecursionLimit, 500)
LUAU_FASTFLAG(LuauKnowsTheDataModel3) LUAU_FASTFLAG(LuauKnowsTheDataModel3)
LUAU_FASTFLAG(LuauAutocompleteDynamicLimits) LUAU_FASTFLAG(LuauAutocompleteDynamicLimits)
LUAU_FASTFLAGVARIABLE(LuauExpectedTableUnionIndexerType, false) LUAU_FASTFLAGVARIABLE(LuauExpectedTableUnionIndexerType, false)
LUAU_FASTFLAGVARIABLE(LuauIndexSilenceErrors, false) LUAU_FASTFLAGVARIABLE(LuauInplaceDemoteSkipAllBound, false)
LUAU_FASTFLAGVARIABLE(LuauLowerBoundsCalculation, false) LUAU_FASTFLAGVARIABLE(LuauLowerBoundsCalculation, false)
LUAU_FASTFLAGVARIABLE(DebugLuauFreezeDuringUnification, false) LUAU_FASTFLAGVARIABLE(DebugLuauFreezeDuringUnification, false)
LUAU_FASTFLAGVARIABLE(LuauSelfCallAutocompleteFix3, false) LUAU_FASTFLAGVARIABLE(LuauSelfCallAutocompleteFix3, false)
LUAU_FASTFLAGVARIABLE(LuauReturnAnyInsteadOfICE, false) // Eventually removed as false. LUAU_FASTFLAGVARIABLE(LuauReturnAnyInsteadOfICE, false) // Eventually removed as false.
LUAU_FASTFLAGVARIABLE(DebugLuauSharedSelf, false); LUAU_FASTFLAGVARIABLE(DebugLuauSharedSelf, false);
LUAU_FASTFLAGVARIABLE(LuauReportErrorsOnIndexerKeyMismatch, false)
LUAU_FASTFLAGVARIABLE(LuauUnknownAndNeverType, false) LUAU_FASTFLAGVARIABLE(LuauUnknownAndNeverType, false)
LUAU_FASTFLAG(LuauQuantifyConstrained)
LUAU_FASTFLAGVARIABLE(LuauFalsyPredicateReturnsNilInstead, false)
LUAU_FASTFLAGVARIABLE(LuauCheckGenericHOFTypes, false) LUAU_FASTFLAGVARIABLE(LuauCheckGenericHOFTypes, false)
LUAU_FASTFLAGVARIABLE(LuauBinaryNeedsExpectedTypesToo, false) LUAU_FASTFLAGVARIABLE(LuauBinaryNeedsExpectedTypesToo, false)
LUAU_FASTFLAGVARIABLE(LuauNeverTypesAndOperatorsInference, false) LUAU_FASTFLAGVARIABLE(LuauNeverTypesAndOperatorsInference, false)
@ -474,7 +471,8 @@ struct InplaceDemoter : TypeVarOnceVisitor
TypeArena* arena; TypeArena* arena;
InplaceDemoter(TypeLevel level, TypeArena* arena) InplaceDemoter(TypeLevel level, TypeArena* arena)
: newLevel(level) : TypeVarOnceVisitor(/* skipBoundTypes= */ FFlag::LuauInplaceDemoteSkipAllBound)
, newLevel(level)
, arena(arena) , arena(arena)
{ {
} }
@ -495,6 +493,7 @@ struct InplaceDemoter : TypeVarOnceVisitor
bool visit(TypeId ty, const BoundTypeVar& btyRef) override bool visit(TypeId ty, const BoundTypeVar& btyRef) override
{ {
LUAU_ASSERT(!FFlag::LuauInplaceDemoteSkipAllBound);
return true; return true;
} }
@ -657,7 +656,7 @@ void TypeChecker::checkBlockWithoutRecursionCheck(const ScopePtr& scope, const A
TypeId leftType = follow(checkFunctionName(scope, *fun->name, funScope->level)); 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>()) else if (auto fun = (*protoIter)->as<AstStatLocalFunction>())
{ {
@ -769,20 +768,20 @@ void TypeChecker::check(const ScopePtr& scope, const AstStatIf& statement)
} }
template<typename Id> 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); 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) 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); 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.unifyLowerBound(subTy, superTy, demotedLevel);
state.log.commit(); state.log.commit();
@ -830,6 +829,14 @@ struct Demoter : Substitution
return get<FreeTypePack>(tp); return get<FreeTypePack>(tp);
} }
bool ignoreChildren(TypeId ty) override
{
if (FFlag::LuauClassTypeVarsInSubstitution && get<ClassTypeVar>(ty))
return true;
return false;
}
TypeId clean(TypeId ty) override TypeId clean(TypeId ty) override
{ {
auto ftv = get<FreeTypeVar>(ty); auto ftv = get<FreeTypeVar>(ty);
@ -851,8 +858,6 @@ struct Demoter : Substitution
void demote(std::vector<std::optional<TypeId>>& expectedTypes) void demote(std::vector<std::optional<TypeId>>& expectedTypes)
{ {
if (!FFlag::LuauQuantifyConstrained)
return;
for (std::optional<TypeId>& ty : expectedTypes) for (std::optional<TypeId>& ty : expectedTypes)
{ {
if (ty) if (ty)
@ -890,7 +895,7 @@ void TypeChecker::check(const ScopePtr& scope, const AstStatReturn& return_)
if (useConstrainedIntersections()) if (useConstrainedIntersections())
{ {
unifyLowerBound(retPack, scope->returnType, demoter.demotedLevel(scope->level), return_.location); unifyLowerBound(retPack, scope->returnType, demoter.demotedLevel(scope->level), scope, return_.location);
return; return;
} }
@ -898,7 +903,7 @@ void TypeChecker::check(const ScopePtr& scope, const AstStatReturn& return_)
// start typechecking everything across module boundaries. // start typechecking everything across module boundaries.
if (isNonstrictMode() && follow(scope->returnType) == follow(currentModule->getModuleScope()->returnType)) 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()) if (!errors.empty())
currentModule->getModuleScope()->returnType = addTypePack({anyType}); currentModule->getModuleScope()->returnType = addTypePack({anyType});
@ -906,13 +911,13 @@ void TypeChecker::check(const ScopePtr& scope, const AstStatReturn& return_)
return; return;
} }
unify(retPack, scope->returnType, return_.location, CountMismatch::Context::Return); unify(retPack, scope->returnType, scope, return_.location, CountMismatch::Context::Return);
} }
template<typename Id> 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) if (FFlag::DebugLuauFreezeDuringUnification)
freeze(currentModule->internalTypes); freeze(currentModule->internalTypes);
@ -928,14 +933,14 @@ ErrorVec TypeChecker::tryUnify_(Id subTy, Id superTy, const Location& location)
return state.errors; 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) 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. // 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))) if (isNonstrictMode() && get<FreeTypeVar>(follow(left)) && !get<FunctionTypeVar>(follow(right)))
unify(anyType, left, loc); unify(anyType, left, scope, loc);
else 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); 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) void TypeChecker::check(const ScopePtr& scope, const AstStatLocal& local)
@ -1101,7 +1106,7 @@ void TypeChecker::check(const ScopePtr& scope, const AstStatLocal& local)
TypePackId valuePack = TypePackId valuePack =
checkExprList(scope, local.location, local.values, /* substituteFreeForNil= */ true, instantiateGenerics, expectedTypes).type; 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.ctx = CountMismatch::Result;
state.tryUnify(valuePack, variablePack); state.tryUnify(valuePack, variablePack);
reportErrors(state.errors); reportErrors(state.errors);
@ -1177,7 +1182,7 @@ void TypeChecker::check(const ScopePtr& scope, const AstStatFor& expr)
TypeId loopVarType = numberType; TypeId loopVarType = numberType;
if (expr.var->annotation) 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}; loopScope->bindings[expr.var] = {loopVarType, expr.var->location};
@ -1187,11 +1192,11 @@ void TypeChecker::check(const ScopePtr& scope, const AstStatFor& expr)
if (!expr.to) if (!expr.to)
ice("Bad AstStatFor has no to expr"); ice("Bad AstStatFor has no to expr");
unify(checkExpr(loopScope, *expr.from).type, loopVarType, expr.from->location); unify(checkExpr(loopScope, *expr.from).type, loopVarType, scope, expr.from->location);
unify(checkExpr(loopScope, *expr.to).type, loopVarType, expr.to->location); unify(checkExpr(loopScope, *expr.to).type, loopVarType, scope, expr.to->location);
if (expr.step) 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); check(loopScope, *expr.body);
} }
@ -1244,12 +1249,12 @@ void TypeChecker::check(const ScopePtr& scope, const AstStatForIn& forin)
if (get<Unifiable::Free>(callRetPack)) if (get<Unifiable::Free>(callRetPack))
{ {
iterTy = freshType(scope); 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)) else if (get<Unifiable::Error>(callRetPack) || !first(callRetPack))
{ {
for (TypeId var : varTypes) for (TypeId var : varTypes)
unify(errorRecoveryType(scope), var, forin.location); unify(errorRecoveryType(scope), var, scope, forin.location);
return check(loopScope, *forin.body); 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 // 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 // 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) for (TypeId var : varTypes)
unify(anyType, var, forin.location); unify(anyType, var, scope, forin.location);
return check(loopScope, *forin.body); return check(loopScope, *forin.body);
} }
@ -1282,25 +1287,25 @@ void TypeChecker::check(const ScopePtr& scope, const AstStatForIn& forin)
if (iterTable->indexer) if (iterTable->indexer)
{ {
if (varTypes.size() > 0) 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) 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) 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()) else if (isNonstrictMode())
{ {
for (TypeId var : varTypes) for (TypeId var : varTypes)
unify(anyType, var, forin.location); unify(anyType, var, scope, forin.location);
} }
else else
{ {
TypeId varTy = errorRecoveryType(loopScope); TypeId varTy = errorRecoveryType(loopScope);
for (TypeId var : varTypes) 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"}); 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); TypeId varTy = get<AnyTypeVar>(iterTy) ? anyType : errorRecoveryType(loopScope);
for (TypeId var : varTypes) 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)) if (!get<ErrorTypeVar>(iterTy) && !get<AnyTypeVar>(iterTy) && !get<FreeTypeVar>(iterTy) && !get<NeverTypeVar>(iterTy))
reportError(firstValue->location, CannotCallNonFunction{iterTy}); reportError(firstValue->location, CannotCallNonFunction{iterTy});
@ -1339,7 +1344,7 @@ void TypeChecker::check(const ScopePtr& scope, const AstStatForIn& forin)
argPack = addTypePack(TypePack{}); argPack = addTypePack(TypePack{});
} }
Unifier state = mkUnifier(firstValue->location); Unifier state = mkUnifier(loopScope, firstValue->location);
checkArgumentList(loopScope, state, argPack, iterFunc->argTypes, /*argLocations*/ {}); checkArgumentList(loopScope, state, argPack, iterFunc->argTypes, /*argLocations*/ {});
state.log.commit(); 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()}; AstExprCall exprCall{Location(start, end), firstValue, arguments, /* self= */ false, Location()};
TypePackId retPack = checkExprPack(scope, exprCall).type; TypePackId retPack = checkExprPack(scope, exprCall).type;
unify(retPack, varPack, forin.location); unify(retPack, varPack, scope, forin.location);
} }
else else
unify(iterFunc->retTypes, varPack, forin.location); unify(iterFunc->retTypes, varPack, scope, forin.location);
check(loopScope, *forin.body); check(loopScope, *forin.body);
} }
@ -1596,7 +1601,7 @@ void TypeChecker::check(const ScopePtr& scope, const AstStatTypeAlias& typealias
TypeId& bindingType = bindingsMap[name].type; TypeId& bindingType = bindingsMap[name].type;
if (unify(ty, bindingType, typealias.location)) if (unify(ty, bindingType, aliasScope, typealias.location))
bindingType = ty; bindingType = ty;
if (FFlag::LuauLowerBoundsCalculation) if (FFlag::LuauLowerBoundsCalculation)
@ -1886,7 +1891,7 @@ WithPredicate<TypeId> TypeChecker::checkExpr(const ScopePtr& scope, const AstExp
TypeLevel level = FFlag::LuauLowerBoundsCalculation ? ftp->level : scope->level; TypeLevel level = FFlag::LuauLowerBoundsCalculation ? ftp->level : scope->level;
TypeId head = freshType(level); TypeId head = freshType(level);
TypePackId pack = addTypePack(TypePackVar{TypePack{{head}, freshTypePack(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)}; return {head, std::move(result.predicates)};
} }
if (get<Unifiable::Error>(retPack)) if (get<Unifiable::Error>(retPack))
@ -1927,7 +1932,7 @@ std::optional<TypeId> TypeChecker::findTablePropertyRespectingMeta(TypeId lhsTyp
{ {
ErrorVec errors; ErrorVec errors;
auto result = Luau::findTablePropertyRespectingMeta(errors, lhsType, name, location); auto result = Luau::findTablePropertyRespectingMeta(errors, lhsType, name, location);
if (!FFlag::LuauIndexSilenceErrors || addErrors) if (addErrors)
reportErrors(errors); reportErrors(errors);
return result; return result;
} }
@ -1936,7 +1941,7 @@ std::optional<TypeId> TypeChecker::findMetatableEntry(TypeId type, std::string e
{ {
ErrorVec errors; ErrorVec errors;
auto result = Luau::findMetatableEntry(errors, type, entry, location); auto result = Luau::findMetatableEntry(errors, type, entry, location);
if (!FFlag::LuauIndexSilenceErrors || addErrors) if (addErrors)
reportErrors(errors); reportErrors(errors);
return result; return result;
} }
@ -1948,7 +1953,7 @@ std::optional<TypeId> TypeChecker::getIndexTypeFromType(
std::optional<TypeId> result = getIndexTypeFromTypeImpl(scope, type, name, location, addErrors); std::optional<TypeId> result = getIndexTypeFromTypeImpl(scope, type, name, location, addErrors);
if (FFlag::LuauIndexSilenceErrors && !addErrors) if (!addErrors)
LUAU_ASSERT(errorCount == currentModule->errors.size()); LUAU_ASSERT(errorCount == currentModule->errors.size());
return result; return result;
@ -1978,20 +1983,15 @@ std::optional<TypeId> TypeChecker::getIndexTypeFromTypeImpl(
else if (auto indexer = tableType->indexer) else if (auto indexer = tableType->indexer)
{ {
// TODO: Property lookup should work with string singletons or unions thereof as the indexer key type. // 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())
{
if (errors.empty())
return indexer->indexResultType;
if (addErrors)
reportError(location, UnknownProperty{type, name});
return std::nullopt;
}
else
return indexer->indexResultType; return indexer->indexResultType;
if (addErrors)
reportError(location, UnknownProperty{type, name});
return std::nullopt;
} }
else if (tableType->state == TableState::Free) else if (tableType->state == TableState::Free)
{ {
@ -2223,8 +2223,8 @@ TypeId TypeChecker::checkExprTable(
if (indexer) if (indexer)
{ {
unify(numberType, indexer->indexType, value->location); unify(numberType, indexer->indexType, scope, value->location);
unify(valueType, indexer->indexResultType, value->location); unify(valueType, indexer->indexResultType, scope, value->location);
} }
else else
indexer = TableIndexer{numberType, anyIfNonstrict(valueType)}; indexer = TableIndexer{numberType, anyIfNonstrict(valueType)};
@ -2243,13 +2243,13 @@ TypeId TypeChecker::checkExprTable(
if (it != expectedTable->props.end()) if (it != expectedTable->props.end())
{ {
Property expectedProp = it->second; Property expectedProp = it->second;
ErrorVec errors = tryUnify(exprType, expectedProp.type, k->location); ErrorVec errors = tryUnify(exprType, expectedProp.type, scope, k->location);
if (errors.empty()) if (errors.empty())
exprType = expectedProp.type; exprType = expectedProp.type;
} }
else if (expectedTable->indexer && maybeString(expectedTable->indexer->indexType)) 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()) if (errors.empty())
exprType = expectedTable->indexer->indexResultType; exprType = expectedTable->indexer->indexResultType;
} }
@ -2264,8 +2264,8 @@ TypeId TypeChecker::checkExprTable(
if (indexer) if (indexer)
{ {
unify(keyType, indexer->indexType, k->location); unify(keyType, indexer->indexType, scope, k->location);
unify(valueType, indexer->indexResultType, value->location); unify(valueType, indexer->indexResultType, scope, value->location);
} }
else if (isNonstrictMode()) else if (isNonstrictMode())
{ {
@ -2406,7 +2406,7 @@ WithPredicate<TypeId> TypeChecker::checkExpr(const ScopePtr& scope, const AstExp
TypePackId retTypePack = freshTypePack(scope); TypePackId retTypePack = freshTypePack(scope);
TypeId expectedFunctionType = addType(FunctionTypeVar(scope->level, arguments, retTypePack)); 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.tryUnify(actualFunctionType, expectedFunctionType, /*isFunctionCall*/ true);
state.log.commit(); state.log.commit();
@ -2424,7 +2424,7 @@ WithPredicate<TypeId> TypeChecker::checkExpr(const ScopePtr& scope, const AstExp
return {errorRecoveryType(scope)}; return {errorRecoveryType(scope)};
} }
reportErrors(tryUnify(operandType, numberType, expr.location)); reportErrors(tryUnify(operandType, numberType, scope, expr.location));
return {numberType}; return {numberType};
} }
case AstExprUnary::Len: case AstExprUnary::Len:
@ -2454,7 +2454,7 @@ WithPredicate<TypeId> TypeChecker::checkExpr(const ScopePtr& scope, const AstExp
TypePackId retTypePack = addTypePack({numberType}); TypePackId retTypePack = addTypePack({numberType});
TypeId expectedFunctionType = addType(FunctionTypeVar(scope->level, arguments, retTypePack)); 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.tryUnify(actualFunctionType, expectedFunctionType, /*isFunctionCall*/ true);
state.log.commit(); 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 (unifyFreeTypes && (get<FreeTypeVar>(a) || get<FreeTypeVar>(b)))
{ {
if (unify(b, a, location)) if (unify(b, a, scope, location))
return a; return a;
return errorRecoveryType(anyType); return errorRecoveryType(anyType);
@ -2583,7 +2583,7 @@ TypeId TypeChecker::checkRelationalOperation(
{ {
ScopePtr subScope = childScope(scope, subexp->location); ScopePtr subScope = childScope(scope, subexp->location);
resolve(predicates, subScope, true); 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 * 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. * have a better, more descriptive error teed up.
*/ */
Unifier state = mkUnifier(expr.location); Unifier state = mkUnifier(scope, expr.location);
if (!isEquality) if (!isEquality)
{ {
state.tryUnify(rhsType, lhsType); state.tryUnify(rhsType, lhsType);
@ -2698,7 +2698,7 @@ TypeId TypeChecker::checkRelationalOperation(
{ {
if (isEquality) if (isEquality)
{ {
Unifier state = mkUnifier(expr.location); Unifier state = mkUnifier(scope, expr.location);
state.tryUnify(addTypePack({booleanType}), ftv->retTypes); state.tryUnify(addTypePack({booleanType}), ftv->retTypes);
if (!state.errors.empty()) if (!state.errors.empty())
@ -2750,11 +2750,11 @@ TypeId TypeChecker::checkRelationalOperation(
case AstExprBinary::And: case AstExprBinary::And:
if (lhsIsAny) if (lhsIsAny)
return lhsType; return lhsType;
return unionOfTypes(rhsType, booleanType, expr.location, false); return unionOfTypes(rhsType, booleanType, scope, expr.location, false);
case AstExprBinary::Or: case AstExprBinary::Or:
if (lhsIsAny) if (lhsIsAny)
return lhsType; return lhsType;
return unionOfTypes(lhsType, rhsType, expr.location); return unionOfTypes(lhsType, rhsType, scope, expr.location);
default: default:
LUAU_ASSERT(0); LUAU_ASSERT(0);
ice(format("checkRelationalOperation called with incorrect binary expression '%s'", toString(expr.op).c_str()), expr.location); 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)) if (get<FreeTypeVar>(rhsType))
unify(rhsType, lhsType, expr.location); unify(rhsType, lhsType, scope, expr.location);
if (typeCouldHaveMetatable(lhsType) || typeCouldHaveMetatable(rhsType)) if (typeCouldHaveMetatable(lhsType) || typeCouldHaveMetatable(rhsType))
{ {
@ -2821,7 +2821,7 @@ TypeId TypeChecker::checkBinaryOperation(
TypePackId retTypePack = freshTypePack(scope); TypePackId retTypePack = freshTypePack(scope);
TypeId expectedFunctionType = addType(FunctionTypeVar(scope->level, arguments, retTypePack)); 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.tryUnify(actualFunctionType, expectedFunctionType, /*isFunctionCall*/ true);
reportErrors(state.errors); reportErrors(state.errors);
@ -2871,8 +2871,8 @@ TypeId TypeChecker::checkBinaryOperation(
switch (expr.op) switch (expr.op)
{ {
case AstExprBinary::Concat: case AstExprBinary::Concat:
reportErrors(tryUnify(lhsType, addType(UnionTypeVar{{stringType, numberType}}), expr.left->location)); reportErrors(tryUnify(lhsType, addType(UnionTypeVar{{stringType, numberType}}), scope, expr.left->location));
reportErrors(tryUnify(rhsType, addType(UnionTypeVar{{stringType, numberType}}), expr.right->location)); reportErrors(tryUnify(rhsType, addType(UnionTypeVar{{stringType, numberType}}), scope, expr.right->location));
return stringType; return stringType;
case AstExprBinary::Add: case AstExprBinary::Add:
case AstExprBinary::Sub: case AstExprBinary::Sub:
@ -2880,8 +2880,8 @@ TypeId TypeChecker::checkBinaryOperation(
case AstExprBinary::Div: case AstExprBinary::Div:
case AstExprBinary::Mod: case AstExprBinary::Mod:
case AstExprBinary::Pow: case AstExprBinary::Pow:
reportErrors(tryUnify(lhsType, numberType, expr.left->location)); reportErrors(tryUnify(lhsType, numberType, scope, expr.left->location));
reportErrors(tryUnify(rhsType, numberType, expr.right->location)); reportErrors(tryUnify(rhsType, numberType, scope, expr.right->location));
return numberType; return numberType;
default: default:
// These should have been handled with checkRelationalOperation // 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); 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. // 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)}; 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)}; return {annotationType, std::move(result.predicates)};
reportError(expr.location, TypesAreUnrelated{result.type, annotationType}); 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) else if (auto indexer = lhsTable->indexer)
{ {
Unifier state = mkUnifier(expr.location); Unifier state = mkUnifier(scope, expr.location);
state.tryUnify(stringType, indexer->indexType); state.tryUnify(stringType, indexer->indexType);
TypeId retType = indexer->indexResultType; TypeId retType = indexer->indexResultType;
if (!state.errors.empty()) if (!state.errors.empty())
@ -3216,7 +3216,7 @@ TypeId TypeChecker::checkLValueBinding(const ScopePtr& scope, const AstExprIndex
if (exprTable->indexer) if (exprTable->indexer)
{ {
const TableIndexer& indexer = *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; return indexer.indexResultType;
} }
else if (exprTable->state == TableState::Unsealed || exprTable->state == TableState::Free) 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 // The use of unify here is deliberate. We don't want this unification
// to be undoable. // to be undoable.
unify(errorRecoveryType(scope), *argIter, state.location); unify(errorRecoveryType(scope), *argIter, scope, state.location);
++argIter; ++argIter;
} }
reportCountMismatchError(); reportCountMismatchError();
@ -3855,7 +3855,7 @@ void TypeChecker::checkArgumentList(
TypeId e = errorRecoveryType(scope); TypeId e = errorRecoveryType(scope);
while (argIter != endIter) while (argIter != endIter)
{ {
unify(e, *argIter, state.location); unify(e, *argIter, scope, state.location);
++argIter; ++argIter;
} }
@ -3872,7 +3872,7 @@ void TypeChecker::checkArgumentList(
if (argIndex < argLocations.size()) if (argIndex < argLocations.size())
location = argLocations[argIndex]; location = argLocations[argIndex];
unify(*argIter, vtp->ty, location); unify(*argIter, vtp->ty, scope, location);
++argIter; ++argIter;
++argIndex; ++argIndex;
} }
@ -3909,7 +3909,7 @@ void TypeChecker::checkArgumentList(
} }
else else
{ {
unifyWithInstantiationIfNeeded(scope, *argIter, *paramIter, state); unifyWithInstantiationIfNeeded(*argIter, *paramIter, scope, state);
++argIter; ++argIter;
++paramIter; ++paramIter;
} }
@ -4117,7 +4117,7 @@ std::optional<WithPredicate<TypePackId>> TypeChecker::checkCallOverload(const Sc
if (get<AnyTypeVar>(fn)) if (get<AnyTypeVar>(fn))
{ {
unify(anyTypePack, argPack, expr.location); unify(anyTypePack, argPack, scope, expr.location);
return {{anyTypePack}}; return {{anyTypePack}};
} }
@ -4163,7 +4163,7 @@ std::optional<WithPredicate<TypePackId>> TypeChecker::checkCallOverload(const Sc
UnifierOptions options; UnifierOptions options;
options.isFunctionCall = true; options.isFunctionCall = true;
unify(r, fn, expr.location, options); unify(r, fn, scope, expr.location, options);
return {{retPack}}; return {{retPack}};
} }
@ -4197,7 +4197,7 @@ std::optional<WithPredicate<TypePackId>> TypeChecker::checkCallOverload(const Sc
if (!ftv) if (!ftv)
{ {
reportError(TypeError{expr.func->location, CannotCallNonFunction{fn}}); 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)}}; return {{errorRecoveryTypePack(retPack)}};
} }
@ -4210,7 +4210,7 @@ std::optional<WithPredicate<TypePackId>> TypeChecker::checkCallOverload(const Sc
return *ret; return *ret;
} }
Unifier state = mkUnifier(expr.location); Unifier state = mkUnifier(scope, expr.location);
// Unify return types // Unify return types
checkArgumentList(scope, state, retPack, ftv->retTypes, /*argLocations*/ {}); 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()); std::vector<TypeId> editedParamList(args->head.begin() + 1, args->head.end());
TypePackId editedArgPack = addTypePack(TypePack{editedParamList}); TypePackId editedArgPack = addTypePack(TypePack{editedParamList});
Unifier editedState = mkUnifier(expr.location); Unifier editedState = mkUnifier(scope, expr.location);
checkArgumentList(scope, editedState, editedArgPack, ftv->argTypes, editedArgLocations); checkArgumentList(scope, editedState, editedArgPack, ftv->argTypes, editedArgLocations);
if (editedState.errors.empty()) 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); editedArgList.insert(editedArgList.begin(), checkExpr(scope, *indexName->expr).type);
TypePackId editedArgPack = addTypePack(TypePack{editedArgList}); TypePackId editedArgPack = addTypePack(TypePack{editedArgList});
Unifier editedState = mkUnifier(expr.location); Unifier editedState = mkUnifier(scope, expr.location);
checkArgumentList(scope, editedState, editedArgPack, ftv->argTypes, editedArgLocations); 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) for (size_t i = 0; i < overloadTypes.size(); ++i)
{ {
TypeId overload = overloadTypes[i]; TypeId overload = overloadTypes[i];
Unifier state = mkUnifier(expr.location); Unifier state = mkUnifier(scope, expr.location);
// Unify return types // Unify return types
if (const FunctionTypeVar* ftv = get<FunctionTypeVar>(overload)) 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; size_t lastIndex = exprs.size - 1;
tp->head.reserve(lastIndex); tp->head.reserve(lastIndex);
Unifier state = mkUnifier(location); Unifier state = mkUnifier(scope, location);
std::vector<TxnLog> inverseLogs; std::vector<TxnLog> inverseLogs;
@ -4583,15 +4583,15 @@ TypeId TypeChecker::anyIfNonstrict(TypeId ty) const
return ty; 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; 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.tryUnify(subTy, superTy, options.isFunctionCall);
state.log.commit(); state.log.commit();
@ -4601,9 +4601,9 @@ bool TypeChecker::unify(TypeId subTy, TypeId superTy, const Location& location,
return state.errors.empty(); 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.ctx = ctx;
state.tryUnify(subTy, superTy); state.tryUnify(subTy, superTy);
@ -4614,10 +4614,10 @@ bool TypeChecker::unify(TypePackId subTy, TypePackId superTy, const Location& lo
return state.errors.empty(); 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); Unifier state = mkUnifier(scope, location);
unifyWithInstantiationIfNeeded(scope, subTy, superTy, state); unifyWithInstantiationIfNeeded(subTy, superTy, scope, state);
state.log.commit(); state.log.commit();
@ -4626,7 +4626,7 @@ bool TypeChecker::unifyWithInstantiationIfNeeded(const ScopePtr& scope, TypeId s
return state.errors.empty(); 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)) if (!maybeGeneric(subTy))
// Quick check to see if we definitely can't instantiate // 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) TypeId TypeChecker::quantify(const ScopePtr& scope, TypeId ty, Location location)
{ {
ty = follow(ty); ty = follow(ty);
@ -4807,7 +4736,7 @@ TypeId TypeChecker::anyify(const ScopePtr& scope, TypeId ty, Location location)
ty = t; ty = t;
} }
Anyification anyification{&currentModule->internalTypes, iceHandler, anyType, anyTypePack}; Anyification anyification{&currentModule->internalTypes, scope, iceHandler, anyType, anyTypePack};
std::optional<TypeId> any = anyification.substitute(ty); std::optional<TypeId> any = anyification.substitute(ty);
if (anyification.normalizationTooComplex) if (anyification.normalizationTooComplex)
reportError(location, NormalizationTooComplex{}); reportError(location, NormalizationTooComplex{});
@ -4830,7 +4759,7 @@ TypePackId TypeChecker::anyify(const ScopePtr& scope, TypePackId ty, Location lo
ty = t; ty = t;
} }
Anyification anyification{&currentModule->internalTypes, iceHandler, anyType, anyTypePack}; Anyification anyification{&currentModule->internalTypes, scope, iceHandler, anyType, anyTypePack};
std::optional<TypePackId> any = anyification.substitute(ty); std::optional<TypePackId> any = anyification.substitute(ty);
if (any.has_value()) if (any.has_value())
return *any; 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) TypeId TypeChecker::freshType(const ScopePtr& scope)
@ -5032,10 +4961,7 @@ TypeIdPredicate TypeChecker::mkTruthyPredicate(bool sense)
return sense ? std::nullopt : std::optional<TypeId>(ty); return sense ? std::nullopt : std::optional<TypeId>(ty);
// at this point, anything else is kept if sense is true, or replaced by nil // at this point, anything else is kept if sense is true, or replaced by nil
if (FFlag::LuauFalsyPredicateReturnsNilInstead) return sense ? ty : nilType;
return sense ? ty : nilType;
else
return sense ? std::optional<TypeId>(ty) : std::nullopt;
}; };
} }
@ -5878,8 +5804,8 @@ void TypeChecker::resolve(const IsAPredicate& isaP, RefinementMap& refis, const
{ {
auto predicate = [&](TypeId option) -> std::optional<TypeId> { 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. // 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 optionIsSubtype = canUnify(option, isaP.ty, scope, isaP.location).empty();
bool targetIsSubtype = canUnify(isaP.ty, option, 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 A is a superset of B, then if sense is true, we promote A to B, otherwise we keep A.
if (!optionIsSubtype && targetIsSubtype) if (!optionIsSubtype && targetIsSubtype)
@ -6022,7 +5948,7 @@ void TypeChecker::resolve(const EqPredicate& eqP, RefinementMap& refis, const Sc
if (maybeSingleton(eqP.type)) if (maybeSingleton(eqP.type))
{ {
// Normally we'd write option <: eqP.type, but singletons are always the subtype, so we flip this. // 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; return sense ? eqP.type : option;
// local variable works around an odd gcc 9.3 warning: <anonymous> may be used uninitialized // 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(); 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 // 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. // 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 // 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/TypeUtils.h"
#include "Luau/Normalize.h"
#include "Luau/Scope.h" #include "Luau/Scope.h"
#include "Luau/ToString.h" #include "Luau/ToString.h"
#include "Luau/TypeInfer.h" #include "Luau/TypeInfer.h"
@ -8,7 +9,7 @@
namespace Luau 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); type = follow(type);
@ -35,7 +36,7 @@ std::optional<TypeId> findMetatableEntry(ErrorVec& errors, TypeId type, std::str
return std::nullopt; 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)) if (get<AnyTypeVar>(ty))
return ty; return ty;
@ -83,4 +84,110 @@ std::optional<TypeId> findTablePropertyRespectingMeta(ErrorVec& errors, TypeId t
return std::nullopt; 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 } // 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; 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 // 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()) if (returnTypes.empty())
return std::nullopt; 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 emptyPack = arena.addTypePack({});
const TypePackId returnList = arena.addTypePack(returnTypes); const TypePackId returnList = arena.addTypePack(returnTypes);
@ -1269,13 +1269,13 @@ static std::optional<WithPredicate<TypePackId>> magicFunctionMatch(
if (returnTypes.empty()) if (returnTypes.empty())
return std::nullopt; 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 optionalNumber = arena.addType(UnionTypeVar{{typechecker.nilType, typechecker.numberType}});
size_t initIndex = expr.self ? 1 : 2; size_t initIndex = expr.self ? 1 : 2;
if (params.size() == 3 && expr.args.size > initIndex) 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); const TypePackId returnList = arena.addTypePack(returnTypes);
return WithPredicate<TypePackId>{returnList}; return WithPredicate<TypePackId>{returnList};
@ -1320,17 +1320,17 @@ static std::optional<WithPredicate<TypePackId>> magicFunctionFind(
return std::nullopt; 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 optionalNumber = arena.addType(UnionTypeVar{{typechecker.nilType, typechecker.numberType}});
const TypeId optionalBoolean = arena.addType(UnionTypeVar{{typechecker.nilType, typechecker.booleanType}}); const TypeId optionalBoolean = arena.addType(UnionTypeVar{{typechecker.nilType, typechecker.booleanType}});
size_t initIndex = expr.self ? 1 : 2; size_t initIndex = expr.self ? 1 : 2;
if (params.size() >= 3 && expr.args.size > initIndex) 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) 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}); returnTypes.insert(returnTypes.begin(), {optionalNumber, optionalNumber});

View file

@ -27,27 +27,6 @@ LUAU_FASTFLAG(DebugLuauFreezeArena)
namespace Luau 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) static size_t pageAlign(size_t size)
{ {
return (size + kPageSize - 1) & ~(kPageSize - 1); return (size + kPageSize - 1) & ~(kPageSize - 1);
@ -55,18 +34,31 @@ static size_t pageAlign(size_t size)
void* pagedAllocate(size_t size) void* pagedAllocate(size_t size)
{ {
if (FFlag::DebugLuauFreezeArena) // By default we use operator new/delete instead of malloc/free so that they can be overridden externally
return systemAllocateAligned(pageAlign(size), kPageSize); if (!FFlag::DebugLuauFreezeArena)
else
return ::operator new(size, std::nothrow); 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) // By default we use operator new/delete instead of malloc/free so that they can be overridden externally
systemDeallocateAligned(ptr); if (!FFlag::DebugLuauFreezeArena)
else return ::operator delete(ptr);
::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) void pagedFreeze(void* ptr, size_t size)

View file

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

View file

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

View file

@ -14,8 +14,6 @@
LUAU_FASTINTVARIABLE(LuauRecursionLimit, 1000) LUAU_FASTINTVARIABLE(LuauRecursionLimit, 1000)
LUAU_FASTINTVARIABLE(LuauParseErrorLimit, 100) LUAU_FASTINTVARIABLE(LuauParseErrorLimit, 100)
LUAU_FASTFLAGVARIABLE(LuauParserFunctionKeywordAsTypeHelp, false)
LUAU_FASTFLAGVARIABLE(LuauFixNamedFunctionParse, false) LUAU_FASTFLAGVARIABLE(LuauFixNamedFunctionParse, false)
LUAU_DYNAMIC_FASTFLAGVARIABLE(LuaReportParseWrongNamedType, 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) , lexer(buffer, bufferSize, names)
, allocator(allocator) , allocator(allocator)
, recursionCounter(0) , recursionCounter(0)
, endMismatchSuspect(Location(), Lexeme::Eof) , endMismatchSuspect(Lexeme(Location(), Lexeme::Eof))
, localMap(AstName()) , localMap(AstName())
{ {
Function top; Function top;
@ -661,7 +659,7 @@ AstStat* Parser::parseFunctionStat()
matchRecoveryStopOnToken[Lexeme::ReservedEnd]++; matchRecoveryStopOnToken[Lexeme::ReservedEnd]++;
AstExprFunction* body = parseFunctionBody(hasself, matchFunction, debugname, {}).first; AstExprFunction* body = parseFunctionBody(hasself, matchFunction, debugname, nullptr).first;
matchRecoveryStopOnToken[Lexeme::ReservedEnd]--; matchRecoveryStopOnToken[Lexeme::ReservedEnd]--;
@ -690,7 +688,7 @@ AstStat* Parser::parseLocal()
matchRecoveryStopOnToken[Lexeme::ReservedEnd]++; matchRecoveryStopOnToken[Lexeme::ReservedEnd]++;
auto [body, var] = parseFunctionBody(false, matchFunction, name.name, name); auto [body, var] = parseFunctionBody(false, matchFunction, name.name, &name);
matchRecoveryStopOnToken[Lexeme::ReservedEnd]--; matchRecoveryStopOnToken[Lexeme::ReservedEnd]--;
@ -782,7 +780,7 @@ AstDeclaredClassProp Parser::parseDeclaredClassMethod()
genericPacks.size = 0; genericPacks.size = 0;
genericPacks.data = nullptr; genericPacks.data = nullptr;
Lexeme matchParen = lexer.current(); MatchLexeme matchParen = lexer.current();
expectAndConsume('(', "function parameter list start"); expectAndConsume('(', "function parameter list start");
TempVector<Binding> args(scratchBinding); TempVector<Binding> args(scratchBinding);
@ -838,7 +836,7 @@ AstStat* Parser::parseDeclaration(const Location& start)
auto [generics, genericPacks] = parseGenericTypeList(/* withDefaultValues= */ false); auto [generics, genericPacks] = parseGenericTypeList(/* withDefaultValues= */ false);
Lexeme matchParen = lexer.current(); MatchLexeme matchParen = lexer.current();
expectAndConsume('(', "global function declaration"); expectAndConsume('(', "global function declaration");
@ -974,13 +972,13 @@ AstStat* Parser::parseCompoundAssignment(AstExpr* initial, AstExprBinary::Op op)
// funcbody ::= `(' [parlist] `)' [`:' ReturnType] block end // funcbody ::= `(' [parlist] `)' [`:' ReturnType] block end
// parlist ::= bindinglist [`,' `...'] | `...' // parlist ::= bindinglist [`,' `...'] | `...'
std::pair<AstExprFunction*, AstLocal*> Parser::parseFunctionBody( 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; Location start = matchFunction.location;
auto [generics, genericPacks] = parseGenericTypeList(/* withDefaultValues= */ false); auto [generics, genericPacks] = parseGenericTypeList(/* withDefaultValues= */ false);
Lexeme matchParen = lexer.current(); MatchLexeme matchParen = lexer.current();
expectAndConsume('(', "function"); expectAndConsume('(', "function");
TempVector<Binding> args(scratchBinding); TempVector<Binding> args(scratchBinding);
@ -992,7 +990,7 @@ std::pair<AstExprFunction*, AstLocal*> Parser::parseFunctionBody(
std::tie(vararg, varargAnnotation) = parseBindingList(args, /* allowDot3= */ true); std::tie(vararg, varargAnnotation) = parseBindingList(args, /* allowDot3= */ true);
std::optional<Location> argLocation = matchParen.type == Lexeme::Type('(') && lexer.current().type == Lexeme::Type(')') 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; : std::nullopt;
expectMatchAndConsume(')', matchParen, true); expectMatchAndConsume(')', matchParen, true);
@ -1259,7 +1257,7 @@ AstType* Parser::parseTableTypeAnnotation()
Location start = lexer.current().location; Location start = lexer.current().location;
Lexeme matchBrace = lexer.current(); MatchLexeme matchBrace = lexer.current();
expectAndConsume('{', "table type"); expectAndConsume('{', "table type");
while (lexer.current().type != '}') while (lexer.current().type != '}')
@ -1638,7 +1636,7 @@ AstTypeOrPack Parser::parseSimpleTypeAnnotation(bool allowPack)
{ {
return parseFunctionTypeAnnotation(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; Location location = lexer.current().location;
@ -1922,14 +1920,14 @@ AstExpr* Parser::parsePrefixExpr()
{ {
if (lexer.current().type == '(') if (lexer.current().type == '(')
{ {
Location start = lexer.current().location; Position start = lexer.current().location.begin;
Lexeme matchParen = lexer.current(); MatchLexeme matchParen = lexer.current();
nextLexeme(); nextLexeme();
AstExpr* expr = parseExpr(); AstExpr* expr = parseExpr();
Location end = lexer.current().location; Position end = lexer.current().location.end;
if (lexer.current().type != ')') if (lexer.current().type != ')')
{ {
@ -1937,7 +1935,7 @@ AstExpr* Parser::parsePrefixExpr()
expectMatchAndConsumeFail(static_cast<Lexeme::Type>(')'), matchParen, suggestion); expectMatchAndConsumeFail(static_cast<Lexeme::Type>(')'), matchParen, suggestion);
end = lexer.previousLocation(); end = lexer.previousLocation().end;
} }
else else
{ {
@ -1955,7 +1953,7 @@ AstExpr* Parser::parsePrefixExpr()
// primaryexp -> prefixexp { `.' NAME | `[' exp `]' | `:' NAME funcargs | funcargs } // primaryexp -> prefixexp { `.' NAME | `[' exp `]' | `:' NAME funcargs | funcargs }
AstExpr* Parser::parsePrimaryExpr(bool asStatement) AstExpr* Parser::parsePrimaryExpr(bool asStatement)
{ {
Location start = lexer.current().location; Position start = lexer.current().location.begin;
AstExpr* expr = parsePrefixExpr(); AstExpr* expr = parsePrefixExpr();
@ -1970,16 +1968,16 @@ AstExpr* Parser::parsePrimaryExpr(bool asStatement)
Name index = parseIndexName(nullptr, opPosition); 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 == '[') else if (lexer.current().type == '[')
{ {
Lexeme matchBracket = lexer.current(); MatchLexeme matchBracket = lexer.current();
nextLexeme(); nextLexeme();
AstExpr* index = parseExpr(); AstExpr* index = parseExpr();
Location end = lexer.current().location; Position end = lexer.current().location.end;
expectMatchAndConsume(']', matchBracket); expectMatchAndConsume(']', matchBracket);
@ -1991,23 +1989,20 @@ AstExpr* Parser::parsePrimaryExpr(bool asStatement)
nextLexeme(); nextLexeme();
Name index = parseIndexName("method name", opPosition); 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 == '(') 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 // 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) if (!asStatement && expr->location.end.line != lexer.current().location.begin.line)
{ {
report(lexer.current().location, reportAmbiguousCallError();
"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");
break; break;
} }
expr = parseFunctionArgs(expr, false, Location()); expr = parseFunctionArgs(expr, false);
} }
else if ( else if (
lexer.current().type == '{' lexer.current().type == '{'
@ -2017,7 +2012,7 @@ AstExpr* Parser::parsePrimaryExpr(bool asStatement)
|| lexer.current().type == Lexeme::InterpStringSimple || lexer.current().type == Lexeme::InterpStringSimple
) )
{ {
expr = parseFunctionArgs(expr, false, Location()); expr = parseFunctionArgs(expr, false);
} }
else else
{ {
@ -2172,7 +2167,7 @@ static ConstantNumberParseResult parseInteger(double& result, const char* data,
return ConstantNumberParseResult::Ok; return ConstantNumberParseResult::Ok;
} }
static ConstantNumberParseResult parseNumber(double& result, const char* data) static ConstantNumberParseResult parseDouble(double& result, const char* data)
{ {
LUAU_ASSERT(FFlag::LuauLintParseIntegerIssues); LUAU_ASSERT(FFlag::LuauLintParseIntegerIssues);
@ -2230,61 +2225,11 @@ AstExpr* Parser::parseSimpleExpr()
Lexeme matchFunction = lexer.current(); Lexeme matchFunction = lexer.current();
nextLexeme(); nextLexeme();
return parseFunctionBody(false, matchFunction, AstName(), {}).first; return parseFunctionBody(false, matchFunction, AstName(), nullptr).first;
} }
else if (lexer.current().type == Lexeme::Number) else if (lexer.current().type == Lexeme::Number)
{ {
scratchData.assign(lexer.current().data, lexer.current().length); return parseNumber();
// 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");
}
}
} }
else if (lexer.current().type == Lexeme::RawString || lexer.current().type == Lexeme::QuotedString || (FFlag::LuauInterpolatedStringBaseSupport && lexer.current().type == Lexeme::InterpStringSimple)) 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 // 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 == '(') if (lexer.current().type == '(')
{ {
Position argStart = lexer.current().location.end; Position argStart = lexer.current().location.end;
if (func->location.end.line != lexer.current().location.begin.line) if (func->location.end.line != lexer.current().location.begin.line)
{ 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");
}
Lexeme matchParen = lexer.current(); MatchLexeme matchParen = lexer.current();
nextLexeme(); nextLexeme();
TempVector<AstExpr*> args(scratchExpr); TempVector<AstExpr*> args(scratchExpr);
@ -2385,18 +2327,29 @@ AstExpr* Parser::parseFunctionArgs(AstExpr* func, bool self, const Location& sel
} }
else else
{ {
if (self && lexer.current().location.begin.line != func->location.end.line) return reportFunctionArgsError(func, self);
{
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 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] `}' // tableconstructor ::= `{' [fieldlist] `}'
// fieldlist ::= field {fieldsep field} [fieldsep] // fieldlist ::= field {fieldsep field} [fieldsep]
// field ::= `[' exp `]' `=' exp | Name `=' exp | exp // field ::= `[' exp `]' `=' exp | Name `=' exp | exp
@ -2407,14 +2360,14 @@ AstExpr* Parser::parseTableConstructor()
Location start = lexer.current().location; Location start = lexer.current().location;
Lexeme matchBrace = lexer.current(); MatchLexeme matchBrace = lexer.current();
expectAndConsume('{', "table literal"); expectAndConsume('{', "table literal");
while (lexer.current().type != '}') while (lexer.current().type != '}')
{ {
if (lexer.current().type == '[') if (lexer.current().type == '[')
{ {
Lexeme matchLocationBracket = lexer.current(); MatchLexeme matchLocationBracket = lexer.current();
nextLexeme(); nextLexeme();
AstExpr* key = parseExpr(); AstExpr* key = parseExpr();
@ -2789,6 +2742,63 @@ AstExpr* Parser::parseInterpString()
} while (true); } 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) AstLocal* Parser::pushLocal(const Binding& binding)
{ {
const Name& name = binding.name; 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()); 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)); 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); expectMatchAndConsumeFail(type, begin);
if (searchForMissing) return expectMatchAndConsumeRecover(value, begin, 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;
} }
else 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 LUAU_NOINLINE bool Parser::expectMatchAndConsumeRecover(char value, const MatchLexeme& begin, bool searchForMissing)
// cold
LUAU_NOINLINE void Parser::expectMatchAndConsumeFail(Lexeme::Type type, const Lexeme& begin, const char* extra)
{ {
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) if (searchForMissing)
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 : ""); // 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 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) 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 // 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 // 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 && if (lexer.current().location.begin.line != begin.position.line &&
lexer.current().location.begin.column != begin.location.begin.column && lexer.current().location.begin.column != begin.position.column &&
endMismatchSuspect.location.begin.line < begin.location.begin.line) // Only replace the previous suspect with more recent suspects endMismatchSuspect.position.line < begin.position.line) // Only replace the previous suspect with more recent suspects
{ {
endMismatchSuspect = begin; 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 // 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 // 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 = std::string matchString = Lexeme(Location(Position(0, 0), 0), endMismatchSuspect.type).toString();
format("; did you forget to close %s at line %d?", endMismatchSuspect.toString().c_str(), endMismatchSuspect.location.begin.line + 1); 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()); 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.Repl.CLI)
add_executable(Luau.Analyze.CLI) add_executable(Luau.Analyze.CLI)
add_executable(Luau.Ast.CLI) add_executable(Luau.Ast.CLI)
add_executable(Luau.Reduce.CLI)
# This also adds target `name` on Linux/macOS and `name.exe` on Windows # 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.Repl.CLI PROPERTIES OUTPUT_NAME luau)
set_target_properties(Luau.Analyze.CLI PROPERTIES OUTPUT_NAME luau-analyze) 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.Ast.CLI PROPERTIES OUTPUT_NAME luau-ast)
set_target_properties(Luau.Reduce.CLI PROPERTIES OUTPUT_NAME luau-reduce)
endif() endif()
if(LUAU_BUILD_TESTS) if(LUAU_BUILD_TESTS)
@ -49,6 +51,7 @@ if(LUAU_BUILD_WEB)
add_executable(Luau.Web) add_executable(Luau.Web)
endif() endif()
include(Sources.cmake) include(Sources.cmake)
target_include_directories(Luau.Common INTERFACE Common/include) 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.Analyze.CLI PRIVATE Luau.Analysis)
target_link_libraries(Luau.Ast.CLI PRIVATE Luau.Ast 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() endif()
if(LUAU_BUILD_TESTS) if(LUAU_BUILD_TESTS)

View file

@ -38,14 +38,21 @@ public:
// Two operand mov instruction has additional specialized encodings // Two operand mov instruction has additional specialized encodings
void mov(OperandX64 lhs, OperandX64 rhs); void mov(OperandX64 lhs, OperandX64 rhs);
void mov64(RegisterX64 lhs, int64_t imm); 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 // Base one operand instruction with 2 opcode selection
void div(OperandX64 op); void div(OperandX64 op);
void idiv(OperandX64 op); void idiv(OperandX64 op);
void mul(OperandX64 op); void mul(OperandX64 op);
void imul(OperandX64 op);
void neg(OperandX64 op); void neg(OperandX64 op);
void not_(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 test(OperandX64 lhs, OperandX64 rhs);
void lea(OperandX64 lhs, OperandX64 rhs); void lea(OperandX64 lhs, OperandX64 rhs);
@ -76,6 +83,12 @@ public:
void vxorpd(OperandX64 dst, OperandX64 src1, OperandX64 src2); void vxorpd(OperandX64 dst, OperandX64 src1, OperandX64 src2);
void vcomisd(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 vsqrtpd(OperandX64 dst, OperandX64 src);
void vsqrtps(OperandX64 dst, OperandX64 src); void vsqrtps(OperandX64 dst, OperandX64 src);
@ -105,6 +118,7 @@ public:
OperandX64 f32(float value); OperandX64 f32(float value);
OperandX64 f64(double value); OperandX64 f64(double value);
OperandX64 f32x4(float x, float y, float z, float w); 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 // 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' // 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, 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 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 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 // Instruction components
void placeRegAndModRegMem(OperandX64 lhs, OperandX64 rhs); 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 op);
LUAU_NOINLINE void log(const char* opcode, OperandX64 op1, OperandX64 op2); 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);
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(Label label);
LUAU_NOINLINE void log(const char* opcode, Label label); LUAU_NOINLINE void log(const char* opcode, Label label);
void log(OperandX64 op); void log(OperandX64 op);

View file

@ -46,6 +46,44 @@ const unsigned AVX_F2 = 0b11;
const unsigned kMaxAlign = 16; 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) AssemblyBuilderX64::AssemblyBuilderX64(bool logText)
: logText(logText) : logText(logText)
{ {
@ -195,6 +233,34 @@ void AssemblyBuilderX64::mov64(RegisterX64 lhs, int64_t imm)
commit(); 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) void AssemblyBuilderX64::div(OperandX64 op)
{ {
placeUnaryModRegMem("div", op, 0xf6, 0xf7, 6); placeUnaryModRegMem("div", op, 0xf6, 0xf7, 6);
@ -210,6 +276,11 @@ void AssemblyBuilderX64::mul(OperandX64 op)
placeUnaryModRegMem("mul", op, 0xf6, 0xf7, 4); placeUnaryModRegMem("mul", op, 0xf6, 0xf7, 4);
} }
void AssemblyBuilderX64::imul(OperandX64 op)
{
placeUnaryModRegMem("imul", op, 0xf6, 0xf7, 5);
}
void AssemblyBuilderX64::neg(OperandX64 op) void AssemblyBuilderX64::neg(OperandX64 op)
{ {
placeUnaryModRegMem("neg", op, 0xf6, 0xf7, 3); placeUnaryModRegMem("neg", op, 0xf6, 0xf7, 3);
@ -220,6 +291,41 @@ void AssemblyBuilderX64::not_(OperandX64 op)
placeUnaryModRegMem("not", op, 0xf6, 0xf7, 2); 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) void AssemblyBuilderX64::test(OperandX64 lhs, OperandX64 rhs)
{ {
// No forms for r/m*, imm8 and reg, r/m* // 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); 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) void AssemblyBuilderX64::vsqrtpd(OperandX64 dst, OperandX64 src)
{ {
placeAvx("vsqrtpd", dst, src, 0x51, false, AVX_0F, AVX_66); placeAvx("vsqrtpd", dst, src, 0x51, false, AVX_0F, AVX_66);
@ -436,7 +562,7 @@ void AssemblyBuilderX64::finalize()
for (Label fixup : pendingLabels) for (Label fixup : pendingLabels)
{ {
uint32_t value = labelLocations[fixup.id - 1] - (fixup.location + 4); 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; size_t dataSize = data.size() - dataPos;
@ -479,34 +605,41 @@ void AssemblyBuilderX64::setLabel(Label& label)
OperandX64 AssemblyBuilderX64::i64(int64_t value) OperandX64 AssemblyBuilderX64::i64(int64_t value)
{ {
size_t pos = allocateData(8, 8); 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())); return OperandX64(SizeX64::qword, noreg, 1, rip, int32_t(pos - data.size()));
} }
OperandX64 AssemblyBuilderX64::f32(float value) OperandX64 AssemblyBuilderX64::f32(float value)
{ {
size_t pos = allocateData(4, 4); 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())); return OperandX64(SizeX64::dword, noreg, 1, rip, int32_t(pos - data.size()));
} }
OperandX64 AssemblyBuilderX64::f64(double value) OperandX64 AssemblyBuilderX64::f64(double value)
{ {
size_t pos = allocateData(8, 8); 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())); return OperandX64(SizeX64::qword, noreg, 1, rip, int32_t(pos - data.size()));
} }
OperandX64 AssemblyBuilderX64::f32x4(float x, float y, float z, float w) OperandX64 AssemblyBuilderX64::f32x4(float x, float y, float z, float w)
{ {
size_t pos = allocateData(16, 16); size_t pos = allocateData(16, 16);
memcpy(&data[pos], &x, sizeof(x)); writef32(&data[pos], x);
memcpy(&data[pos + 4], &y, sizeof(y)); writef32(&data[pos + 4], y);
memcpy(&data[pos + 8], &z, sizeof(z)); writef32(&data[pos + 8], z);
memcpy(&data[pos + 12], &w, sizeof(w)); writef32(&data[pos + 12], w);
return OperandX64(SizeX64::xmmword, noreg, 1, rip, int32_t(pos - data.size())); 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, 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) uint8_t code8rev, uint8_t coderev, uint8_t code8, uint8_t code, uint8_t opreg)
{ {
@ -700,6 +833,24 @@ void AssemblyBuilderX64::placeAvx(
commit(); 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) void AssemblyBuilderX64::placeRex(RegisterX64 op)
{ {
uint8_t code = REX_W(op.size == SizeX64::qword) | REX_B(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) void AssemblyBuilderX64::placeImm32(int32_t imm)
{ {
LUAU_ASSERT(codePos + sizeof(imm) < codeEnd); uint8_t* pos = codePos;
memcpy(codePos, &imm, sizeof(imm)); LUAU_ASSERT(pos + sizeof(imm) < codeEnd);
codePos += sizeof(imm); writeu32(pos, imm);
codePos = pos + sizeof(imm);
} }
void AssemblyBuilderX64::placeImm64(int64_t imm) void AssemblyBuilderX64::placeImm64(int64_t imm)
{ {
LUAU_ASSERT(codePos + sizeof(imm) < codeEnd); uint8_t* pos = codePos;
memcpy(codePos, &imm, sizeof(imm)); LUAU_ASSERT(pos + sizeof(imm) < codeEnd);
codePos += sizeof(imm); writeu64(pos, imm);
codePos = pos + sizeof(imm);
} }
void AssemblyBuilderX64::placeLabel(Label& label) void AssemblyBuilderX64::placeLabel(Label& label)
@ -970,6 +1123,19 @@ void AssemblyBuilderX64::log(const char* opcode, OperandX64 op1, OperandX64 op2,
text.append("\n"); 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) void AssemblyBuilderX64::log(Label label)
{ {
logAppend(".L%d:\n", label.id); logAppend(".L%d:\n", label.id);

View file

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

View file

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

View file

@ -24,13 +24,14 @@ LUAU_FASTINTVARIABLE(LuauCompileInlineThreshold, 25)
LUAU_FASTINTVARIABLE(LuauCompileInlineThresholdMaxBoost, 300) LUAU_FASTINTVARIABLE(LuauCompileInlineThresholdMaxBoost, 300)
LUAU_FASTINTVARIABLE(LuauCompileInlineDepth, 5) LUAU_FASTINTVARIABLE(LuauCompileInlineDepth, 5)
LUAU_FASTFLAGVARIABLE(LuauCompileNoIpairs, false)
LUAU_FASTFLAGVARIABLE(LuauCompileFreeReassign, false)
LUAU_FASTFLAGVARIABLE(LuauCompileXEQ, false) LUAU_FASTFLAGVARIABLE(LuauCompileXEQ, false)
LUAU_FASTFLAG(LuauInterpolatedStringBaseSupport) LUAU_FASTFLAG(LuauInterpolatedStringBaseSupport)
LUAU_FASTFLAGVARIABLE(LuauCompileOptimalAssignment, false)
LUAU_FASTFLAGVARIABLE(LuauCompileExtractK, false)
namespace Luau namespace Luau
{ {
@ -40,6 +41,8 @@ static const uint32_t kMaxRegisterCount = 255;
static const uint32_t kMaxUpvalueCount = 200; static const uint32_t kMaxUpvalueCount = 200;
static const uint32_t kMaxLocalCount = 200; static const uint32_t kMaxLocalCount = 200;
static const uint8_t kInvalidReg = 255;
CompileError::CompileError(const Location& location, const std::string& message) CompileError::CompileError(const Location& location, const std::string& message)
: location(location) : location(location)
, message(message) , 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->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; 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) 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) 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) if (int reg = getExprLocalReg(expr->args.data[i]); reg >= 0)
{
args[i] = uint8_t(reg); args[i] = uint8_t(reg);
}
else else
{ {
args[i] = uint8_t(regs + 1 + i); args[i] = uint8_t(regs + 1 + i);
@ -436,21 +460,31 @@ struct Compiler
bytecode.emitABC(opc, uint8_t(bfid), uint8_t(args[0]), 0); bytecode.emitABC(opc, uint8_t(bfid), uint8_t(args[0]), 0);
if (opc != LOP_FASTCALL1) 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. // 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 // Note, as with other instructions that immediately follow FASTCALL, these are normally not executed and are used as a fallback for
// these FASTCALL variants. // these FASTCALL variants.
for (size_t i = 0; i < expr->args.size; ++i) 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]); if (i > 0 && opc == LOP_FASTCALL2K)
break; 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) if (args[i] != regs + 1 + i)
bytecode.emitABC(LOP_MOVE, uint8_t(regs + 1 + i), uint8_t(args[i]), 0); 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 // note, these instructions are normally not executed and are used as a fallback for FASTCALL
@ -599,7 +633,7 @@ struct Compiler
} }
else else
{ {
AstExprLocal* le = FFlag::LuauCompileFreeReassign ? getExprLocal(arg) : arg->as<AstExprLocal>(); AstExprLocal* le = getExprLocal(arg);
Variable* lv = le ? variables.find(le->local) : nullptr; 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 // 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; 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 // 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])) 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); 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) // 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 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 and b transforms to !a ? a : b
// onlyTruth = 0: a or b transforms to !a ? b : dontcare // onlyTruth = 0: a or b transforms to !a ? b : dontcare
if (onlyTruth == (expr->op == AstExprBinary::And)) if (onlyTruth == (expr->op == AstExprBinary::And))
@ -2107,9 +2161,35 @@ struct Compiler
return reg; 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 // 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 is multret, we assume it 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 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 // 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) 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) for (size_t i = 0; i < list.size - 1; ++i)
compileExprTemp(list.data[i], uint8_t(target + i)); compileExprTemp(list.data[i], uint8_t(target + i));
AstExpr* last = list.data[list.size - 1]; compileExprTempN(list.data[list.size - 1], uint8_t(target + list.size - 1), uint8_t(targetCount - (list.size - 1)), targetTop);
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);
}
} }
else else
{ {
@ -2639,7 +2703,7 @@ struct Compiler
return; return;
// Optimization: for 1-1 local assignments, we can reuse the register *if* neither local is mutated // 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])) if (AstExprLocal* re = getExprLocal(stat->values.data[0]))
{ {
@ -2853,7 +2917,6 @@ struct Compiler
LUAU_ASSERT(vars == regs + 3); LUAU_ASSERT(vars == regs + 3);
LuauOpcode skipOp = LOP_FORGPREP; 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 // 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 // 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); Builtin builtin = getBuiltin(stat->values.data[0]->as<AstExprCall>()->func, globals, variables);
if (builtin.isGlobal("ipairs")) // for .. in ipairs(t) if (builtin.isGlobal("ipairs")) // for .. in ipairs(t)
{
skipOp = LOP_FORGPREP_INEXT; skipOp = LOP_FORGPREP_INEXT;
loopOp = FFlag::LuauCompileNoIpairs ? LOP_FORGLOOP : LOP_FORGLOOP_INEXT;
}
else if (builtin.isGlobal("pairs")) // for .. in pairs(t) else if (builtin.isGlobal("pairs")) // for .. in pairs(t)
{
skipOp = LOP_FORGPREP_NEXT; skipOp = LOP_FORGPREP_NEXT;
loopOp = LOP_FORGLOOP;
}
} }
else if (stat->values.size == 2) else if (stat->values.size == 2)
{ {
Builtin builtin = getBuiltin(stat->values.data[0], globals, variables); Builtin builtin = getBuiltin(stat->values.data[0], globals, variables);
if (builtin.isGlobal("next")) // for .. in next,t if (builtin.isGlobal("next")) // for .. in next,t
{
skipOp = LOP_FORGPREP_NEXT; skipOp = LOP_FORGPREP_NEXT;
loopOp = LOP_FORGLOOP;
}
} }
} }
@ -2909,19 +2963,9 @@ struct Compiler
size_t backLabel = bytecode.emitLabel(); size_t backLabel = bytecode.emitLabel();
bytecode.emitAD(loopOp, regs, 0); // FORGLOOP uses aux to encode variable count and fast path flag for ipairs traversal in the high bit
bytecode.emitAD(LOP_FORGLOOP, regs, 0);
if (FFlag::LuauCompileNoIpairs) bytecode.emitAux((skipOp == LOP_FORGPREP_INEXT ? 0x80000000 : 0) | uint32_t(stat->vars.size));
{
// 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));
size_t endLabel = bytecode.emitLabel(); size_t endLabel = bytecode.emitLabel();
@ -2936,6 +2980,8 @@ struct Compiler
void resolveAssignConflicts(AstStat* stat, std::vector<LValue>& vars) 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 // 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 // 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] // 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) void compileStatAssign(AstStatAssign* stat)
{ {
RegScope rs(this); RegScope rs(this);
// Optimization: one to one assignments don't require complex conflict resolution machinery and allow us to skip temporary registers for // Optimization: one to one assignments don't require complex conflict resolution machinery
// locals
if (stat->vars.size == 1 && stat->values.size == 1) if (stat->vars.size == 1 && stat->values.size == 1)
{ {
LValue var = compileLValue(stat->vars.data[0], rs); LValue var = compileLValue(stat->vars.data[0], rs);
@ -3013,28 +3133,110 @@ struct Compiler
return; return;
} }
// compute all l-values: note that this doesn't assign anything yet but it allocates registers and computes complex expressions on the left if (FFlag::LuauCompileOptimalAssignment)
// 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]); // compute all l-values: note that this doesn't assign anything yet but it allocates registers and computes complex expressions on the
compileAssign(vars[i], uint8_t(regs + i)); // 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 LDFLAGS+=-fsanitize=address,fuzzer
endif endif
ifneq ($(CALLGRIND),) ifeq ($(config),profile)
CXXFLAGS+=-DCALLGRIND=$(CALLGRIND) CXXFLAGS+=-O2 -DNDEBUG -gdwarf-4 -DCALLGRIND=1
endif endif
# target-specific flags # target-specific flags

View file

@ -66,6 +66,7 @@ target_sources(Luau.CodeGen PRIVATE
# Luau.Analysis Sources # Luau.Analysis Sources
target_sources(Luau.Analysis PRIVATE target_sources(Luau.Analysis PRIVATE
Analysis/include/Luau/Anyification.h
Analysis/include/Luau/ApplyTypeFunction.h Analysis/include/Luau/ApplyTypeFunction.h
Analysis/include/Luau/AstJsonEncoder.h Analysis/include/Luau/AstJsonEncoder.h
Analysis/include/Luau/AstQuery.h Analysis/include/Luau/AstQuery.h
@ -115,6 +116,7 @@ target_sources(Luau.Analysis PRIVATE
Analysis/include/Luau/Variant.h Analysis/include/Luau/Variant.h
Analysis/include/Luau/VisitTypeVar.h Analysis/include/Luau/VisitTypeVar.h
Analysis/src/Anyification.cpp
Analysis/src/ApplyTypeFunction.cpp Analysis/src/ApplyTypeFunction.cpp
Analysis/src/AstJsonEncoder.cpp Analysis/src/AstJsonEncoder.cpp
Analysis/src/AstQuery.cpp Analysis/src/AstQuery.cpp
@ -126,6 +128,7 @@ target_sources(Luau.Analysis PRIVATE
Analysis/src/ConstraintGraphBuilder.cpp Analysis/src/ConstraintGraphBuilder.cpp
Analysis/src/ConstraintSolver.cpp Analysis/src/ConstraintSolver.cpp
Analysis/src/ConstraintSolverLogger.cpp Analysis/src/ConstraintSolverLogger.cpp
Analysis/src/EmbeddedBuiltinDefinitions.cpp
Analysis/src/Error.cpp Analysis/src/Error.cpp
Analysis/src/Frontend.cpp Analysis/src/Frontend.cpp
Analysis/src/Instantiation.cpp Analysis/src/Instantiation.cpp
@ -155,7 +158,6 @@ target_sources(Luau.Analysis PRIVATE
Analysis/src/TypeVar.cpp Analysis/src/TypeVar.cpp
Analysis/src/Unifiable.cpp Analysis/src/Unifiable.cpp
Analysis/src/Unifier.cpp Analysis/src/Unifier.cpp
Analysis/src/EmbeddedBuiltinDefinitions.cpp
) )
# Luau.VM Sources # Luau.VM Sources
@ -347,3 +349,11 @@ if(TARGET Luau.Web)
target_sources(Luau.Web PRIVATE target_sources(Luau.Web PRIVATE
CLI/Web.cpp) CLI/Web.cpp)
endif() 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_checkGC(L);
luaC_checkthreadsleep(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)); memcpy(&u->data + sz, &dtor, sizeof(dtor));
setuvalue(L, L->top, u); setuvalue(L, L->top, u);
api_incr_top(L); api_incr_top(L);

View file

@ -15,6 +15,8 @@
#include <intrin.h> #include <intrin.h>
#endif #endif
LUAU_FASTFLAGVARIABLE(LuauFasterBit32NoWidth, false)
// luauF functions implement FASTCALL instruction that performs a direct execution of some builtin functions from the VM // 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. // 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 // 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) 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 a1 = nvalue(arg0);
double a2 = nvalue(args); double a2 = nvalue(args);
double a3 = nvalue(args + 1);
unsigned n; unsigned n;
luai_num2unsigned(n, a1); luai_num2unsigned(n, a1);
int f = int(a2); 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)); if (unsigned(f) < 32)
uint32_t r = (n >> f) & m; {
uint32_t m = 1;
uint32_t r = (n >> f) & m;
setnvalue(res, double(r)); setnvalue(res, double(r));
return 1; 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) 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 a1 = nvalue(arg0);
double a2 = nvalue(args); double a2 = nvalue(args);
double a3 = nvalue(args + 1); double a3 = nvalue(args + 1);
double a4 = nvalue(args + 2);
unsigned n, v; unsigned n, v;
luai_num2unsigned(n, a1); luai_num2unsigned(n, a1);
luai_num2unsigned(v, a2); luai_num2unsigned(v, a2);
int f = int(a3); 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)); if (unsigned(f) < 32)
uint32_t r = (n & ~(m << f)) | ((v & m) << f); {
uint32_t m = 1;
uint32_t r = (n & ~(m << f)) | ((v & m) << f);
setnvalue(res, double(r)); setnvalue(res, double(r));
return 1; 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; 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] = { luau_FastFunction luauF_table[256] = {
NULL, NULL,
luauF_assert, luauF_assert,
@ -1211,4 +1268,6 @@ luau_FastFunction luauF_table[256] = {
luauF_select, luauF_select,
luauF_rawlen, 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) const LocVar* luaF_getlocal(const Proto* f, int local_number, int pc)
{ {
int i; for (int i = 0; i < f->sizelocvars; i++)
for (i = 0; i < f->sizelocvars; i++)
{ {
if (pc >= f->locvars[i].startpc && pc < f->locvars[i].endpc) if (pc >= f->locvars[i].startpc && pc < f->locvars[i].endpc)
{ // is variable active? { // is variable active?
@ -185,5 +184,15 @@ const LocVar* luaF_getlocal(const Proto* f, int local_number, int pc)
return &f->locvars[i]; 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 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_unlinkupval(UpVal* uv);
LUAI_FUNC void luaF_freeupval(lua_State* L, UpVal* uv, struct lua_Page* page); 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_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->isC)
{ {
if (cl->c.debugname)
fprintf(f, ",\"name\":\"%s\"", cl->c.debugname + 0);
if (cl->nupvalues) if (cl->nupvalues)
{ {
fprintf(f, ",\"upvalues\":["); fprintf(f, ",\"upvalues\":[");
@ -354,6 +357,9 @@ static void dumpclosure(FILE* f, Closure* cl)
} }
else else
{ {
if (cl->l.p->debugname)
fprintf(f, ",\"name\":\"%s\"", getstr(cl->l.p->debugname));
fprintf(f, ",\"proto\":"); fprintf(f, ",\"proto\":");
dumpref(f, obj2gco(cl->l.p)); dumpref(f, obj2gco(cl->l.p));
if (cl->nupvalues) if (cl->nupvalues)
@ -403,7 +409,7 @@ static void dumpthread(FILE* f, lua_State* th)
fprintf(f, ",\"source\":\""); fprintf(f, ",\"source\":\"");
dumpstringdata(f, p->source->data, p->source->len); 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) if (th->top > th->stack)
@ -411,6 +417,55 @@ static void dumpthread(FILE* f, lua_State* th)
fprintf(f, ",\"stack\":["); fprintf(f, ",\"stack\":[");
dumprefs(f, th->stack, th->top - th->stack); dumprefs(f, th->stack, th->top - th->stack);
fprintf(f, "]"); 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, "}"); fprintf(f, "}");
} }

View file

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

View file

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

View file

@ -70,6 +70,7 @@ Sandboxing challenges are [covered in the dedicated section](sandbox).
| `bit32` library | ✔️ | | | `bit32` library | ✔️ | |
| `string.gsub` is stricter about using `%` on special characters only | ✔️ | | | `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 | | 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. 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 --!nostrict
-- Unknown comment directive 'nostrict'; did you mean 'nonstrict'?" -- 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: 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 ```lua
if getmetatable(gen) and getmetatable(gen).__iter then local genmt = rawgetmetatable(gen) -- pseudo code for getmetatable that bypasses __metatable
gen, state, index = getmetatable(gen).__iter(gen) local iterf = genmt and rawget(genmt, "__iter")
if iterf then
gen, state, index = iterf(gen)
end end
``` ```

View file

@ -22,7 +22,7 @@ std::string bytecodeAsArray(const std::vector<uint8_t>& bytecode)
class AssemblyBuilderX64Fixture class AssemblyBuilderX64Fixture
{ {
public: 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); AssemblyBuilderX64 build(/* logText= */ false);
@ -30,9 +30,15 @@ public:
build.finalize(); 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); CHECK(false);
} }
} }
@ -169,6 +175,7 @@ TEST_CASE_FIXTURE(AssemblyBuilderX64Fixture, "BaseUnaryInstructionForms")
SINGLE_COMPARE(div(rcx), 0x48, 0xf7, 0xf1); SINGLE_COMPARE(div(rcx), 0x48, 0xf7, 0xf1);
SINGLE_COMPARE(idiv(qword[rax]), 0x48, 0xf7, 0x38); SINGLE_COMPARE(idiv(qword[rax]), 0x48, 0xf7, 0x38);
SINGLE_COMPARE(mul(qword[rax + rbx]), 0x48, 0xf7, 0x24, 0x18); 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(neg(r9), 0x49, 0xf7, 0xd9);
SINGLE_COMPARE(not_(r12), 0x49, 0xf7, 0xd4); SINGLE_COMPARE(not_(r12), 0x49, 0xf7, 0xd4);
} }
@ -191,6 +198,18 @@ TEST_CASE_FIXTURE(AssemblyBuilderX64Fixture, "FormsOfMov")
SINGLE_COMPARE(mov(byte[rsi], al), 0x88, 0x06); 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") TEST_CASE_FIXTURE(AssemblyBuilderX64Fixture, "FormsOfTest")
{ {
SINGLE_COMPARE(test(al, 8), 0xf6, 0xc0, 0x08); 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); 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") TEST_CASE_FIXTURE(AssemblyBuilderX64Fixture, "ControlFlow")
{ {
// Jump back // Jump back
@ -335,6 +367,7 @@ TEST_CASE_FIXTURE(AssemblyBuilderX64Fixture, "AVXUnaryMergeInstructionForms")
// Coverage for other instructions that follow the same pattern // Coverage for other instructions that follow the same pattern
SINGLE_COMPARE(vcomisd(xmm8, xmm10), 0xc4, 0x41, 0xf9, 0x2f, 0xc2); 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") 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); 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") TEST_CASE_FIXTURE(AssemblyBuilderX64Fixture, "MiscInstructions")
{ {
SINGLE_COMPARE(int3(), 0xcc); SINGLE_COMPARE(int3(), 0xcc);
@ -386,6 +438,11 @@ TEST_CASE("LogTest")
build.neg(qword[rbp + r12 * 2]); build.neg(qword[rbp + r12 * 2]);
build.mov64(r10, 0x1234567812345678ll); build.mov64(r10, 0x1234567812345678ll);
build.vmovapd(xmmword[rax], xmm11); 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.pop(r12);
build.ret(); build.ret();
build.int3(); build.int3();
@ -409,6 +466,11 @@ TEST_CASE("LogTest")
neg qword ptr [rbp+r12*2] neg qword ptr [rbp+r12*2]
mov r10,1234567812345678h mov r10,1234567812345678h
vmovapd xmmword ptr [rax],xmm11 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 pop r12
ret ret
int3 int3
@ -426,6 +488,8 @@ TEST_CASE_FIXTURE(AssemblyBuilderX64Fixture, "Constants")
build.vmovss(xmm2, build.f32(1.0f)); build.vmovss(xmm2, build.f32(1.0f));
build.vmovsd(xmm3, build.f64(1.0)); build.vmovsd(xmm3, build.f64(1.0));
build.vmovaps(xmm4, build.f32x4(1.0f, 2.0f, 4.0f, 8.0f)); 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(); build.ret();
}, },
{ {
@ -434,7 +498,20 @@ TEST_CASE_FIXTURE(AssemblyBuilderX64Fixture, "Constants")
0xc4, 0xe1, 0xfa, 0x10, 0x15, 0xe1, 0xff, 0xff, 0xff, 0xc4, 0xe1, 0xfa, 0x10, 0x15, 0xe1, 0xff, 0xff, 0xff,
0xc4, 0xe1, 0xfb, 0x10, 0x1d, 0xcc, 0xff, 0xff, 0xff, 0xc4, 0xe1, 0xfb, 0x10, 0x1d, 0xcc, 0xff, 0xff, 0xff,
0xc4, 0xe1, 0xf8, 0x28, 0x25, 0xab, 0xff, 0xff, 0xff, 0xc4, 0xe1, 0xf8, 0x28, 0x25, 0xab, 0xff, 0xff, 0xff,
0xc4, 0xe1, 0xf9, 0x10, 0x2d, 0x92, 0xff, 0xff, 0xff,
0xc3 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 // clang-format on
} }
@ -444,7 +521,7 @@ TEST_CASE("ConstantStorage")
AssemblyBuilderX64 build(/* logText= */ false); AssemblyBuilderX64 build(/* logText= */ false);
for (int i = 0; i <= 3000; i++) 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(); build.finalize();
@ -452,9 +529,10 @@ TEST_CASE("ConstantStorage")
for (int i = 0; i <= 3000; i++) for (int i = 0; i <= 3000; i++)
{ {
float v; LUAU_ASSERT(build.data[i * 4 + 0] == 0x00);
memcpy(&v, &build.data[build.data.size() - (i + 1) * sizeof(float)], sizeof(v)); LUAU_ASSERT(build.data[i * 4 + 1] == 0x00);
LUAU_ASSERT(v == float(i)); 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.empty());
CHECK(ac.entryMap.count("table")); CHECK(ac.entryMap.count("table"));
CHECK(ac.entryMap.count("math")); CHECK(ac.entryMap.count("math"));
CHECK_EQ(ac.context, AutocompleteContext::Statement);
} }
TEST_CASE_FIXTURE(ACFixture, "local_initializer") TEST_CASE_FIXTURE(ACFixture, "local_initializer")
@ -138,6 +139,7 @@ TEST_CASE_FIXTURE(ACFixture, "local_initializer")
auto ac = autocomplete('1'); auto ac = autocomplete('1');
CHECK(ac.entryMap.count("table")); CHECK(ac.entryMap.count("table"));
CHECK(ac.entryMap.count("math")); CHECK(ac.entryMap.count("math"));
CHECK_EQ(ac.context, AutocompleteContext::Expression);
} }
TEST_CASE_FIXTURE(ACFixture, "leave_numbers_alone") TEST_CASE_FIXTURE(ACFixture, "leave_numbers_alone")
@ -146,6 +148,7 @@ TEST_CASE_FIXTURE(ACFixture, "leave_numbers_alone")
auto ac = autocomplete('1'); auto ac = autocomplete('1');
CHECK(ac.entryMap.empty()); CHECK(ac.entryMap.empty());
CHECK_EQ(ac.context, AutocompleteContext::Unknown);
} }
TEST_CASE_FIXTURE(ACFixture, "user_defined_globals") 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("myLocal"));
CHECK(ac.entryMap.count("table")); CHECK(ac.entryMap.count("table"));
CHECK(ac.entryMap.count("math")); CHECK(ac.entryMap.count("math"));
CHECK_EQ(ac.context, AutocompleteContext::Statement);
} }
TEST_CASE_FIXTURE(ACFixture, "dont_suggest_local_before_its_definition") TEST_CASE_FIXTURE(ACFixture, "dont_suggest_local_before_its_definition")
@ -191,6 +195,7 @@ TEST_CASE_FIXTURE(ACFixture, "recursive_function")
auto ac = autocomplete('1'); auto ac = autocomplete('1');
CHECK(ac.entryMap.count("foo")); CHECK(ac.entryMap.count("foo"));
CHECK_EQ(ac.context, AutocompleteContext::Statement);
} }
TEST_CASE_FIXTURE(ACFixture, "nested_recursive_function") 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("find"));
CHECK(ac.entryMap.count("pack")); CHECK(ac.entryMap.count("pack"));
CHECK(!ac.entryMap.count("math")); CHECK(!ac.entryMap.count("math"));
CHECK_EQ(ac.context, AutocompleteContext::Property);
} }
TEST_CASE_FIXTURE(ACFixture, "nested_member_completions") 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_EQ(2, ac.entryMap.size());
CHECK(ac.entryMap.count("def")); CHECK(ac.entryMap.count("def"));
CHECK(ac.entryMap.count("egh")); CHECK(ac.entryMap.count("egh"));
CHECK_EQ(ac.context, AutocompleteContext::Property);
} }
TEST_CASE_FIXTURE(ACFixture, "unsealed_table") TEST_CASE_FIXTURE(ACFixture, "unsealed_table")
@ -319,6 +326,7 @@ TEST_CASE_FIXTURE(ACFixture, "unsealed_table")
auto ac = autocomplete('1'); auto ac = autocomplete('1');
CHECK_EQ(1, ac.entryMap.size()); CHECK_EQ(1, ac.entryMap.size());
CHECK(ac.entryMap.count("prop")); CHECK(ac.entryMap.count("prop"));
CHECK_EQ(ac.context, AutocompleteContext::Property);
} }
TEST_CASE_FIXTURE(ACFixture, "unsealed_table_2") TEST_CASE_FIXTURE(ACFixture, "unsealed_table_2")
@ -333,6 +341,7 @@ TEST_CASE_FIXTURE(ACFixture, "unsealed_table_2")
auto ac = autocomplete('1'); auto ac = autocomplete('1');
CHECK_EQ(1, ac.entryMap.size()); CHECK_EQ(1, ac.entryMap.size());
CHECK(ac.entryMap.count("prop")); CHECK(ac.entryMap.count("prop"));
CHECK_EQ(ac.context, AutocompleteContext::Property);
} }
TEST_CASE_FIXTURE(ACFixture, "cyclic_table") TEST_CASE_FIXTURE(ACFixture, "cyclic_table")
@ -346,6 +355,7 @@ TEST_CASE_FIXTURE(ACFixture, "cyclic_table")
auto ac = autocomplete('1'); auto ac = autocomplete('1');
CHECK(ac.entryMap.count("abc")); CHECK(ac.entryMap.count("abc"));
CHECK_EQ(ac.context, AutocompleteContext::Property);
} }
TEST_CASE_FIXTURE(ACFixture, "table_union") TEST_CASE_FIXTURE(ACFixture, "table_union")
@ -361,6 +371,7 @@ TEST_CASE_FIXTURE(ACFixture, "table_union")
auto ac = autocomplete('1'); auto ac = autocomplete('1');
CHECK_EQ(1, ac.entryMap.size()); CHECK_EQ(1, ac.entryMap.size());
CHECK(ac.entryMap.count("b2")); CHECK(ac.entryMap.count("b2"));
CHECK_EQ(ac.context, AutocompleteContext::Property);
} }
TEST_CASE_FIXTURE(ACFixture, "table_intersection") 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("a1"));
CHECK(ac.entryMap.count("b2")); CHECK(ac.entryMap.count("b2"));
CHECK(ac.entryMap.count("c3")); CHECK(ac.entryMap.count("c3"));
CHECK_EQ(ac.context, AutocompleteContext::Property);
} }
TEST_CASE_FIXTURE(ACBuiltinsFixture, "get_string_completions") TEST_CASE_FIXTURE(ACBuiltinsFixture, "get_string_completions")
@ -389,6 +401,7 @@ TEST_CASE_FIXTURE(ACBuiltinsFixture, "get_string_completions")
auto ac = autocomplete('1'); auto ac = autocomplete('1');
CHECK_EQ(17, ac.entryMap.size()); CHECK_EQ(17, ac.entryMap.size());
CHECK_EQ(ac.context, AutocompleteContext::Property);
} }
TEST_CASE_FIXTURE(ACFixture, "get_suggestions_for_new_statement") 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_NE(0, ac.entryMap.size());
CHECK(ac.entryMap.count("table")); CHECK(ac.entryMap.count("table"));
CHECK_EQ(ac.context, AutocompleteContext::Statement);
} }
TEST_CASE_FIXTURE(ACFixture, "get_suggestions_for_the_very_start_of_the_script") 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'); auto ac = autocomplete('1');
CHECK(ac.entryMap.count("table")); CHECK(ac.entryMap.count("table"));
CHECK_EQ(ac.context, AutocompleteContext::Statement);
} }
TEST_CASE_FIXTURE(ACFixture, "method_call_inside_function_body") 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_NE(0, ac.entryMap.size());
CHECK(!ac.entryMap.count("math")); CHECK(!ac.entryMap.count("math"));
CHECK_EQ(ac.context, AutocompleteContext::Property);
} }
TEST_CASE_FIXTURE(ACBuiltinsFixture, "method_call_inside_if_conditional") 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_NE(0, ac.entryMap.size());
CHECK(ac.entryMap.count("concat")); CHECK(ac.entryMap.count("concat"));
CHECK(!ac.entryMap.count("math")); CHECK(!ac.entryMap.count("math"));
CHECK_EQ(ac.context, AutocompleteContext::Property);
} }
TEST_CASE_FIXTURE(ACFixture, "statement_between_two_statements") 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_NE(0, ac.entryMap.size());
CHECK(ac.entryMap.count("getmyscripts")); CHECK(ac.entryMap.count("getmyscripts"));
CHECK_EQ(ac.context, AutocompleteContext::Statement);
} }
TEST_CASE_FIXTURE(ACFixture, "bias_toward_inner_scope") TEST_CASE_FIXTURE(ACFixture, "bias_toward_inner_scope")
@ -476,6 +495,7 @@ TEST_CASE_FIXTURE(ACFixture, "bias_toward_inner_scope")
auto ac = autocomplete('1'); auto ac = autocomplete('1');
CHECK(ac.entryMap.count("A")); CHECK(ac.entryMap.count("A"));
CHECK_EQ(ac.context, AutocompleteContext::Statement);
TypeId t = follow(*ac.entryMap["A"].type); TypeId t = follow(*ac.entryMap["A"].type);
const TableTypeVar* tt = get<TableTypeVar>(t); const TableTypeVar* tt = get<TableTypeVar>(t);
@ -489,10 +509,12 @@ TEST_CASE_FIXTURE(ACFixture, "recommend_statement_starting_keywords")
check("@1"); check("@1");
auto ac = autocomplete('1'); auto ac = autocomplete('1');
CHECK(ac.entryMap.count("local")); CHECK(ac.entryMap.count("local"));
CHECK_EQ(ac.context, AutocompleteContext::Statement);
check("local i = @1"); check("local i = @1");
auto ac2 = autocomplete('1'); auto ac2 = autocomplete('1');
CHECK(!ac2.entryMap.count("local")); CHECK(!ac2.entryMap.count("local"));
CHECK_EQ(ac2.context, AutocompleteContext::Expression);
} }
TEST_CASE_FIXTURE(ACFixture, "do_not_overwrite_context_sensitive_kws") 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"]; AutocompleteEntry entry = ac.entryMap["continue"];
CHECK(entry.kind == AutocompleteEntryKind::Binding); CHECK(entry.kind == AutocompleteEntryKind::Binding);
CHECK_EQ(ac.context, AutocompleteContext::Statement);
} }
TEST_CASE_FIXTURE(ACFixture, "dont_offer_any_suggestions_from_within_a_comment") 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'); auto ac = autocomplete('1');
CHECK_EQ(0, ac.entryMap.size()); 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") 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'); auto ac = autocomplete('1');
CHECK_EQ(0, ac.entryMap.size()); 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") 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'); auto ac = autocomplete('1');
CHECK_EQ(0, ac.entryMap.size()); 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") 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'); auto ac = autocomplete('1');
CHECK_EQ(0, ac.entryMap.size()); CHECK_EQ(0, ac.entryMap.size());
CHECK_EQ(ac.context, AutocompleteContext::Unknown);
} }
TEST_CASE_FIXTURE(ACFixture, "autocomplete_for_middle_keywords") TEST_CASE_FIXTURE(ACFixture, "autocomplete_for_middle_keywords")
@ -566,6 +593,7 @@ TEST_CASE_FIXTURE(ACFixture, "autocomplete_for_middle_keywords")
auto ac1 = autocomplete('1'); auto ac1 = autocomplete('1');
CHECK_EQ(ac1.entryMap.count("do"), 0); CHECK_EQ(ac1.entryMap.count("do"), 0);
CHECK_EQ(ac1.entryMap.count("end"), 0); CHECK_EQ(ac1.entryMap.count("end"), 0);
CHECK_EQ(ac1.context, AutocompleteContext::Unknown);
check(R"( check(R"(
for x =@1 1 for x =@1 1
@ -574,6 +602,7 @@ TEST_CASE_FIXTURE(ACFixture, "autocomplete_for_middle_keywords")
auto ac2 = autocomplete('1'); auto ac2 = autocomplete('1');
CHECK_EQ(ac2.entryMap.count("do"), 0); CHECK_EQ(ac2.entryMap.count("do"), 0);
CHECK_EQ(ac2.entryMap.count("end"), 0); CHECK_EQ(ac2.entryMap.count("end"), 0);
CHECK_EQ(ac2.context, AutocompleteContext::Unknown);
check(R"( check(R"(
for x = 1,@1 2 for x = 1,@1 2
@ -582,6 +611,7 @@ TEST_CASE_FIXTURE(ACFixture, "autocomplete_for_middle_keywords")
auto ac3 = autocomplete('1'); auto ac3 = autocomplete('1');
CHECK_EQ(1, ac3.entryMap.size()); CHECK_EQ(1, ac3.entryMap.size());
CHECK_EQ(ac3.entryMap.count("do"), 1); CHECK_EQ(ac3.entryMap.count("do"), 1);
CHECK_EQ(ac3.context, AutocompleteContext::Keyword);
check(R"( check(R"(
for x = 1, @12, for x = 1, @12,
@ -590,6 +620,7 @@ TEST_CASE_FIXTURE(ACFixture, "autocomplete_for_middle_keywords")
auto ac4 = autocomplete('1'); auto ac4 = autocomplete('1');
CHECK_EQ(ac4.entryMap.count("do"), 0); CHECK_EQ(ac4.entryMap.count("do"), 0);
CHECK_EQ(ac4.entryMap.count("end"), 0); CHECK_EQ(ac4.entryMap.count("end"), 0);
CHECK_EQ(ac4.context, AutocompleteContext::Expression);
check(R"( check(R"(
for x = 1, 2, @15 for x = 1, 2, @15
@ -598,6 +629,7 @@ TEST_CASE_FIXTURE(ACFixture, "autocomplete_for_middle_keywords")
auto ac5 = autocomplete('1'); auto ac5 = autocomplete('1');
CHECK_EQ(ac5.entryMap.count("do"), 1); CHECK_EQ(ac5.entryMap.count("do"), 1);
CHECK_EQ(ac5.entryMap.count("end"), 0); CHECK_EQ(ac5.entryMap.count("end"), 0);
CHECK_EQ(ac5.context, AutocompleteContext::Keyword);
check(R"( check(R"(
for x = 1, 2, 5 f@1 for x = 1, 2, 5 f@1
@ -606,6 +638,7 @@ TEST_CASE_FIXTURE(ACFixture, "autocomplete_for_middle_keywords")
auto ac6 = autocomplete('1'); auto ac6 = autocomplete('1');
CHECK_EQ(ac6.entryMap.size(), 1); CHECK_EQ(ac6.entryMap.size(), 1);
CHECK_EQ(ac6.entryMap.count("do"), 1); CHECK_EQ(ac6.entryMap.count("do"), 1);
CHECK_EQ(ac6.context, AutocompleteContext::Keyword);
check(R"( check(R"(
for x = 1, 2, 5 do @1 for x = 1, 2, 5 do @1
@ -613,6 +646,7 @@ TEST_CASE_FIXTURE(ACFixture, "autocomplete_for_middle_keywords")
auto ac7 = autocomplete('1'); auto ac7 = autocomplete('1');
CHECK_EQ(ac7.entryMap.count("end"), 1); CHECK_EQ(ac7.entryMap.count("end"), 1);
CHECK_EQ(ac7.context, AutocompleteContext::Statement);
} }
TEST_CASE_FIXTURE(ACFixture, "autocomplete_for_in_middle_keywords") 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'); auto ac1 = autocomplete('1');
CHECK_EQ(0, ac1.entryMap.size()); CHECK_EQ(0, ac1.entryMap.size());
CHECK_EQ(ac1.context, AutocompleteContext::Unknown);
check(R"( check(R"(
for x@1 @2 for x@1 @2
@ -630,10 +665,12 @@ TEST_CASE_FIXTURE(ACFixture, "autocomplete_for_in_middle_keywords")
auto ac2 = autocomplete('1'); auto ac2 = autocomplete('1');
CHECK_EQ(0, ac2.entryMap.size()); CHECK_EQ(0, ac2.entryMap.size());
CHECK_EQ(ac2.context, AutocompleteContext::Unknown);
auto ac2a = autocomplete('2'); auto ac2a = autocomplete('2');
CHECK_EQ(1, ac2a.entryMap.size()); CHECK_EQ(1, ac2a.entryMap.size());
CHECK_EQ(1, ac2a.entryMap.count("in")); CHECK_EQ(1, ac2a.entryMap.count("in"));
CHECK_EQ(ac2a.context, AutocompleteContext::Keyword);
check(R"( check(R"(
for x in y@1 for x in y@1
@ -642,6 +679,7 @@ TEST_CASE_FIXTURE(ACFixture, "autocomplete_for_in_middle_keywords")
auto ac3 = autocomplete('1'); auto ac3 = autocomplete('1');
CHECK_EQ(ac3.entryMap.count("table"), 1); CHECK_EQ(ac3.entryMap.count("table"), 1);
CHECK_EQ(ac3.entryMap.count("do"), 0); CHECK_EQ(ac3.entryMap.count("do"), 0);
CHECK_EQ(ac3.context, AutocompleteContext::Expression);
check(R"( check(R"(
for x in y @1 for x in y @1
@ -650,6 +688,7 @@ TEST_CASE_FIXTURE(ACFixture, "autocomplete_for_in_middle_keywords")
auto ac4 = autocomplete('1'); auto ac4 = autocomplete('1');
CHECK_EQ(ac4.entryMap.size(), 1); CHECK_EQ(ac4.entryMap.size(), 1);
CHECK_EQ(ac4.entryMap.count("do"), 1); CHECK_EQ(ac4.entryMap.count("do"), 1);
CHECK_EQ(ac4.context, AutocompleteContext::Keyword);
check(R"( check(R"(
for x in f f@1 for x in f f@1
@ -658,6 +697,7 @@ TEST_CASE_FIXTURE(ACFixture, "autocomplete_for_in_middle_keywords")
auto ac5 = autocomplete('1'); auto ac5 = autocomplete('1');
CHECK_EQ(ac5.entryMap.size(), 1); CHECK_EQ(ac5.entryMap.size(), 1);
CHECK_EQ(ac5.entryMap.count("do"), 1); CHECK_EQ(ac5.entryMap.count("do"), 1);
CHECK_EQ(ac5.context, AutocompleteContext::Keyword);
check(R"( check(R"(
for x in y do @1 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("table"), 1);
CHECK_EQ(ac6.entryMap.count("end"), 1); CHECK_EQ(ac6.entryMap.count("end"), 1);
CHECK_EQ(ac6.entryMap.count("function"), 1); CHECK_EQ(ac6.entryMap.count("function"), 1);
CHECK_EQ(ac6.context, AutocompleteContext::Statement);
check(R"( check(R"(
for x in y do e@1 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("table"), 1);
CHECK_EQ(ac7.entryMap.count("end"), 1); CHECK_EQ(ac7.entryMap.count("end"), 1);
CHECK_EQ(ac7.entryMap.count("function"), 1); CHECK_EQ(ac7.entryMap.count("function"), 1);
CHECK_EQ(ac7.context, AutocompleteContext::Statement);
} }
TEST_CASE_FIXTURE(ACFixture, "autocomplete_while_middle_keywords") TEST_CASE_FIXTURE(ACFixture, "autocomplete_while_middle_keywords")
@ -689,6 +731,7 @@ TEST_CASE_FIXTURE(ACFixture, "autocomplete_while_middle_keywords")
auto ac1 = autocomplete('1'); auto ac1 = autocomplete('1');
CHECK_EQ(ac1.entryMap.count("do"), 0); CHECK_EQ(ac1.entryMap.count("do"), 0);
CHECK_EQ(ac1.entryMap.count("end"), 0); CHECK_EQ(ac1.entryMap.count("end"), 0);
CHECK_EQ(ac1.context, AutocompleteContext::Expression);
check(R"( check(R"(
while true @1 while true @1
@ -697,6 +740,7 @@ TEST_CASE_FIXTURE(ACFixture, "autocomplete_while_middle_keywords")
auto ac2 = autocomplete('1'); auto ac2 = autocomplete('1');
CHECK_EQ(1, ac2.entryMap.size()); CHECK_EQ(1, ac2.entryMap.size());
CHECK_EQ(ac2.entryMap.count("do"), 1); CHECK_EQ(ac2.entryMap.count("do"), 1);
CHECK_EQ(ac2.context, AutocompleteContext::Keyword);
check(R"( check(R"(
while true do @1 while true do @1
@ -704,6 +748,7 @@ TEST_CASE_FIXTURE(ACFixture, "autocomplete_while_middle_keywords")
auto ac3 = autocomplete('1'); auto ac3 = autocomplete('1');
CHECK_EQ(ac3.entryMap.count("end"), 1); CHECK_EQ(ac3.entryMap.count("end"), 1);
CHECK_EQ(ac3.context, AutocompleteContext::Statement);
check(R"( check(R"(
while true d@1 while true d@1
@ -712,6 +757,7 @@ TEST_CASE_FIXTURE(ACFixture, "autocomplete_while_middle_keywords")
auto ac4 = autocomplete('1'); auto ac4 = autocomplete('1');
CHECK_EQ(1, ac4.entryMap.size()); CHECK_EQ(1, ac4.entryMap.size());
CHECK_EQ(ac4.entryMap.count("do"), 1); CHECK_EQ(ac4.entryMap.count("do"), 1);
CHECK_EQ(ac4.context, AutocompleteContext::Keyword);
} }
TEST_CASE_FIXTURE(ACFixture, "autocomplete_if_middle_keywords") 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("else"), 0);
CHECK_EQ(ac1.entryMap.count("elseif"), 0); CHECK_EQ(ac1.entryMap.count("elseif"), 0);
CHECK_EQ(ac1.entryMap.count("end"), 0); CHECK_EQ(ac1.entryMap.count("end"), 0);
CHECK_EQ(ac1.context, AutocompleteContext::Expression);
check(R"( check(R"(
if x @1 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("else"), 0);
CHECK_EQ(ac2.entryMap.count("elseif"), 0); CHECK_EQ(ac2.entryMap.count("elseif"), 0);
CHECK_EQ(ac2.entryMap.count("end"), 0); CHECK_EQ(ac2.entryMap.count("end"), 0);
CHECK_EQ(ac2.context, AutocompleteContext::Keyword);
check(R"( check(R"(
if x t@1 if x t@1
@ -747,6 +795,7 @@ TEST_CASE_FIXTURE(ACFixture, "autocomplete_if_middle_keywords")
auto ac3 = autocomplete('1'); auto ac3 = autocomplete('1');
CHECK_EQ(1, ac3.entryMap.size()); CHECK_EQ(1, ac3.entryMap.size());
CHECK_EQ(ac3.entryMap.count("then"), 1); CHECK_EQ(ac3.entryMap.count("then"), 1);
CHECK_EQ(ac3.context, AutocompleteContext::Keyword);
check(R"( check(R"(
if x then 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("function"), 1);
CHECK_EQ(ac4.entryMap.count("elseif"), 1); CHECK_EQ(ac4.entryMap.count("elseif"), 1);
CHECK_EQ(ac4.entryMap.count("end"), 0); CHECK_EQ(ac4.entryMap.count("end"), 0);
CHECK_EQ(ac4.context, AutocompleteContext::Statement);
check(R"( check(R"(
if x then 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("table"), 1);
CHECK_EQ(ac4a.entryMap.count("else"), 1); CHECK_EQ(ac4a.entryMap.count("else"), 1);
CHECK_EQ(ac4a.entryMap.count("elseif"), 1); CHECK_EQ(ac4a.entryMap.count("elseif"), 1);
CHECK_EQ(ac4a.context, AutocompleteContext::Statement);
check(R"( check(R"(
if x then 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("else"), 0);
CHECK_EQ(ac5.entryMap.count("elseif"), 0); CHECK_EQ(ac5.entryMap.count("elseif"), 0);
CHECK_EQ(ac5.entryMap.count("end"), 0); CHECK_EQ(ac5.entryMap.count("end"), 0);
CHECK_EQ(ac5.context, AutocompleteContext::Statement);
} }
TEST_CASE_FIXTURE(ACFixture, "autocomplete_until_in_repeat") TEST_CASE_FIXTURE(ACFixture, "autocomplete_until_in_repeat")
@ -797,6 +849,7 @@ TEST_CASE_FIXTURE(ACFixture, "autocomplete_until_in_repeat")
auto ac = autocomplete('1'); auto ac = autocomplete('1');
CHECK_EQ(ac.entryMap.count("table"), 1); CHECK_EQ(ac.entryMap.count("table"), 1);
CHECK_EQ(ac.entryMap.count("until"), 1); CHECK_EQ(ac.entryMap.count("until"), 1);
CHECK_EQ(ac.context, AutocompleteContext::Statement);
} }
TEST_CASE_FIXTURE(ACFixture, "autocomplete_until_expression") TEST_CASE_FIXTURE(ACFixture, "autocomplete_until_expression")
@ -808,6 +861,7 @@ TEST_CASE_FIXTURE(ACFixture, "autocomplete_until_expression")
auto ac = autocomplete('1'); auto ac = autocomplete('1');
CHECK_EQ(ac.entryMap.count("table"), 1); CHECK_EQ(ac.entryMap.count("table"), 1);
CHECK_EQ(ac.context, AutocompleteContext::Expression);
} }
TEST_CASE_FIXTURE(ACFixture, "local_names") TEST_CASE_FIXTURE(ACFixture, "local_names")
@ -819,6 +873,7 @@ TEST_CASE_FIXTURE(ACFixture, "local_names")
auto ac1 = autocomplete('1'); auto ac1 = autocomplete('1');
CHECK_EQ(ac1.entryMap.size(), 1); CHECK_EQ(ac1.entryMap.size(), 1);
CHECK_EQ(ac1.entryMap.count("function"), 1); CHECK_EQ(ac1.entryMap.count("function"), 1);
CHECK_EQ(ac1.context, AutocompleteContext::Unknown);
check(R"( check(R"(
local ab, cd@1 local ab, cd@1
@ -826,6 +881,7 @@ TEST_CASE_FIXTURE(ACFixture, "local_names")
auto ac2 = autocomplete('1'); auto ac2 = autocomplete('1');
CHECK(ac2.entryMap.empty()); CHECK(ac2.entryMap.empty());
CHECK_EQ(ac2.context, AutocompleteContext::Unknown);
} }
TEST_CASE_FIXTURE(ACFixture, "autocomplete_end_with_fn_exprs") 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'); auto ac = autocomplete('1');
CHECK_EQ(ac.entryMap.count("end"), 1); CHECK_EQ(ac.entryMap.count("end"), 1);
CHECK_EQ(ac.context, AutocompleteContext::Statement);
} }
TEST_CASE_FIXTURE(ACFixture, "autocomplete_end_with_lambda") TEST_CASE_FIXTURE(ACFixture, "autocomplete_end_with_lambda")
@ -846,6 +903,7 @@ TEST_CASE_FIXTURE(ACFixture, "autocomplete_end_with_lambda")
auto ac = autocomplete('1'); auto ac = autocomplete('1');
CHECK_EQ(ac.entryMap.count("end"), 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") 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'); auto ac1 = autocomplete('1');
CHECK_EQ(ac1.entryMap.count("in"), 1); CHECK_EQ(ac1.entryMap.count("in"), 1);
CHECK_EQ(ac1.entryMap.count("until"), 0); CHECK_EQ(ac1.entryMap.count("until"), 0);
CHECK_EQ(ac1.context, AutocompleteContext::Keyword);
} }
TEST_CASE_FIXTURE(ACFixture, "autocomplete_repeat_middle_keyword") TEST_CASE_FIXTURE(ACFixture, "autocomplete_repeat_middle_keyword")
@ -980,6 +1039,7 @@ TEST_CASE_FIXTURE(ACFixture, "local_function_params")
auto ac2 = autocomplete('1'); auto ac2 = autocomplete('1');
CHECK_EQ(ac2.entryMap.count("abc"), 1); CHECK_EQ(ac2.entryMap.count("abc"), 1);
CHECK_EQ(ac2.entryMap.count("def"), 1); CHECK_EQ(ac2.entryMap.count("def"), 1);
CHECK_EQ(ac2.context, AutocompleteContext::Statement);
check(R"( check(R"(
local function abc(def, ghi@1) local function abc(def, ghi@1)
@ -988,6 +1048,7 @@ TEST_CASE_FIXTURE(ACFixture, "local_function_params")
auto ac3 = autocomplete('1'); auto ac3 = autocomplete('1');
CHECK(ac3.entryMap.empty()); CHECK(ac3.entryMap.empty());
CHECK_EQ(ac3.context, AutocompleteContext::Unknown);
} }
TEST_CASE_FIXTURE(ACFixture, "global_function_params") TEST_CASE_FIXTURE(ACFixture, "global_function_params")
@ -1022,6 +1083,7 @@ TEST_CASE_FIXTURE(ACFixture, "global_function_params")
auto ac2 = autocomplete('1'); auto ac2 = autocomplete('1');
CHECK_EQ(ac2.entryMap.count("abc"), 1); CHECK_EQ(ac2.entryMap.count("abc"), 1);
CHECK_EQ(ac2.entryMap.count("def"), 1); CHECK_EQ(ac2.entryMap.count("def"), 1);
CHECK_EQ(ac2.context, AutocompleteContext::Statement);
check(R"( check(R"(
function abc(def, ghi@1) function abc(def, ghi@1)
@ -1030,6 +1092,7 @@ TEST_CASE_FIXTURE(ACFixture, "global_function_params")
auto ac3 = autocomplete('1'); auto ac3 = autocomplete('1');
CHECK(ac3.entryMap.empty()); CHECK(ac3.entryMap.empty());
CHECK_EQ(ac3.context, AutocompleteContext::Unknown);
} }
TEST_CASE_FIXTURE(ACFixture, "arguments_to_global_lambda") TEST_CASE_FIXTURE(ACFixture, "arguments_to_global_lambda")
@ -1074,6 +1137,7 @@ TEST_CASE_FIXTURE(ACFixture, "function_expr_params")
auto ac2 = autocomplete('1'); auto ac2 = autocomplete('1');
CHECK_EQ(ac2.entryMap.count("def"), 1); CHECK_EQ(ac2.entryMap.count("def"), 1);
CHECK_EQ(ac2.context, AutocompleteContext::Statement);
} }
TEST_CASE_FIXTURE(ACFixture, "local_initializer") 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("nil"));
CHECK(ac.entryMap.count("number")); CHECK(ac.entryMap.count("number"));
CHECK_EQ(ac.context, AutocompleteContext::Type);
} }
TEST_CASE_FIXTURE(ACFixture, "private_types") TEST_CASE_FIXTURE(ACFixture, "private_types")
@ -1203,6 +1268,7 @@ local a: aa
auto ac = Luau::autocomplete(frontend, "Module/B", Position{2, 11}, nullCallback); auto ac = Luau::autocomplete(frontend, "Module/B", Position{2, 11}, nullCallback);
CHECK(ac.entryMap.count("aaa")); CHECK(ac.entryMap.count("aaa"));
CHECK_EQ(ac.context, AutocompleteContext::Type);
} }
TEST_CASE_FIXTURE(ACFixture, "module_type_members") TEST_CASE_FIXTURE(ACFixture, "module_type_members")
@ -1227,6 +1293,7 @@ local a: aaa.
CHECK_EQ(2, ac.entryMap.size()); CHECK_EQ(2, ac.entryMap.size());
CHECK(ac.entryMap.count("A")); CHECK(ac.entryMap.count("A"));
CHECK(ac.entryMap.count("B")); CHECK(ac.entryMap.count("B"));
CHECK_EQ(ac.context, AutocompleteContext::Type);
} }
TEST_CASE_FIXTURE(ACFixture, "argument_types") 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("nil"));
CHECK(ac.entryMap.count("number")); CHECK(ac.entryMap.count("number"));
CHECK_EQ(ac.context, AutocompleteContext::Type);
} }
TEST_CASE_FIXTURE(ACFixture, "return_types") 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("nil"));
CHECK(ac.entryMap.count("number")); CHECK(ac.entryMap.count("number"));
CHECK_EQ(ac.context, AutocompleteContext::Type);
} }
TEST_CASE_FIXTURE(ACFixture, "as_types") 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("nil"));
CHECK(ac.entryMap.count("number")); CHECK(ac.entryMap.count("number"));
CHECK_EQ(ac.context, AutocompleteContext::Type);
} }
TEST_CASE_FIXTURE(ACFixture, "function_type_types") TEST_CASE_FIXTURE(ACFixture, "function_type_types")
@ -1314,6 +1384,7 @@ local b: string = "don't trip"
auto ac = autocomplete('1'); auto ac = autocomplete('1');
CHECK(ac.entryMap.count("Tee")); CHECK(ac.entryMap.count("Tee"));
CHECK_EQ(ac.context, AutocompleteContext::Type);
} }
TEST_CASE_FIXTURE(ACFixture, "type_correct_suggestion_in_argument") 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.count("one"));
CHECK(ac.entryMap["one"].typeCorrect == TypeCorrectKind::Correct); CHECK(ac.entryMap["one"].typeCorrect == TypeCorrectKind::Correct);
CHECK(ac.entryMap["two"].typeCorrect == TypeCorrectKind::None); CHECK(ac.entryMap["two"].typeCorrect == TypeCorrectKind::None);
CHECK_EQ(ac.context, AutocompleteContext::Property);
check(R"( check(R"(
type Foo = { a: number, b: string } type Foo = { a: number, b: string }
@ -1414,6 +1486,7 @@ local b: Foo = { b = a.@1
CHECK(ac.entryMap.count("two")); CHECK(ac.entryMap.count("two"));
CHECK(ac.entryMap["two"].typeCorrect == TypeCorrectKind::Correct); CHECK(ac.entryMap["two"].typeCorrect == TypeCorrectKind::Correct);
CHECK(ac.entryMap["one"].typeCorrect == TypeCorrectKind::None); CHECK(ac.entryMap["one"].typeCorrect == TypeCorrectKind::None);
CHECK_EQ(ac.context, AutocompleteContext::Property);
} }
TEST_CASE_FIXTURE(ACFixture, "type_correct_function_return_types") TEST_CASE_FIXTURE(ACFixture, "type_correct_function_return_types")
@ -2395,6 +2468,7 @@ local t: Test = { f@1 }
auto ac = autocomplete('1'); auto ac = autocomplete('1');
CHECK(ac.entryMap.count("first")); CHECK(ac.entryMap.count("first"));
CHECK(ac.entryMap.count("second")); CHECK(ac.entryMap.count("second"));
CHECK_EQ(ac.context, AutocompleteContext::Property);
// Intersection // Intersection
check(R"( check(R"(
@ -2405,6 +2479,7 @@ local t: Test = { f@1 }
ac = autocomplete('1'); ac = autocomplete('1');
CHECK(ac.entryMap.count("first")); CHECK(ac.entryMap.count("first"));
CHECK(ac.entryMap.count("second")); CHECK(ac.entryMap.count("second"));
CHECK_EQ(ac.context, AutocompleteContext::Property);
// Union // Union
check(R"( check(R"(
@ -2416,6 +2491,7 @@ local t: Test = { s@1 }
CHECK(ac.entryMap.count("second")); CHECK(ac.entryMap.count("second"));
CHECK(!ac.entryMap.count("first")); CHECK(!ac.entryMap.count("first"));
CHECK(!ac.entryMap.count("third")); CHECK(!ac.entryMap.count("third"));
CHECK_EQ(ac.context, AutocompleteContext::Property);
// No parenthesis suggestion // No parenthesis suggestion
check(R"( check(R"(
@ -2426,6 +2502,7 @@ local t: Test = { f@1 }
ac = autocomplete('1'); ac = autocomplete('1');
CHECK(ac.entryMap.count("first")); CHECK(ac.entryMap.count("first"));
CHECK(ac.entryMap["first"].parens == ParenthesesRecommendation::None); CHECK(ac.entryMap["first"].parens == ParenthesesRecommendation::None);
CHECK_EQ(ac.context, AutocompleteContext::Property);
// When key is changed // When key is changed
check(R"( check(R"(
@ -2436,6 +2513,7 @@ local t: Test = { f@1 = 2 }
ac = autocomplete('1'); ac = autocomplete('1');
CHECK(ac.entryMap.count("first")); CHECK(ac.entryMap.count("first"));
CHECK(ac.entryMap.count("second")); CHECK(ac.entryMap.count("second"));
CHECK_EQ(ac.context, AutocompleteContext::Property);
// Alternative key syntax // Alternative key syntax
check(R"( check(R"(
@ -2446,6 +2524,7 @@ local t: Test = { ["f@1"] }
ac = autocomplete('1'); ac = autocomplete('1');
CHECK(ac.entryMap.count("first")); CHECK(ac.entryMap.count("first"));
CHECK(ac.entryMap.count("second")); CHECK(ac.entryMap.count("second"));
CHECK_EQ(ac.context, AutocompleteContext::Property);
// Not an alternative key syntax // Not an alternative key syntax
check(R"( check(R"(
@ -2456,6 +2535,7 @@ local t: Test = { "f@1" }
ac = autocomplete('1'); ac = autocomplete('1');
CHECK(!ac.entryMap.count("first")); CHECK(!ac.entryMap.count("first"));
CHECK(!ac.entryMap.count("second")); CHECK(!ac.entryMap.count("second"));
CHECK_EQ(ac.context, AutocompleteContext::String);
// Skip keys that are already defined // Skip keys that are already defined
check(R"( check(R"(
@ -2466,6 +2546,7 @@ local t: Test = { first = 2, s@1 }
ac = autocomplete('1'); ac = autocomplete('1');
CHECK(!ac.entryMap.count("first")); CHECK(!ac.entryMap.count("first"));
CHECK(ac.entryMap.count("second")); CHECK(ac.entryMap.count("second"));
CHECK_EQ(ac.context, AutocompleteContext::Property);
// Don't skip active key // Don't skip active key
check(R"( check(R"(
@ -2476,6 +2557,7 @@ local t: Test = { first@1 }
ac = autocomplete('1'); ac = autocomplete('1');
CHECK(ac.entryMap.count("first")); CHECK(ac.entryMap.count("first"));
CHECK(ac.entryMap.count("second")); CHECK(ac.entryMap.count("second"));
CHECK_EQ(ac.context, AutocompleteContext::Property);
// Inference after first key // Inference after first key
check(R"( check(R"(
@ -2488,6 +2570,7 @@ local t = {
ac = autocomplete('1'); ac = autocomplete('1');
CHECK(ac.entryMap.count("first")); CHECK(ac.entryMap.count("first"));
CHECK(ac.entryMap.count("second")); CHECK(ac.entryMap.count("second"));
CHECK_EQ(ac.context, AutocompleteContext::Property);
check(R"( check(R"(
local t = { local t = {
@ -2499,6 +2582,7 @@ local t = {
ac = autocomplete('1'); ac = autocomplete('1');
CHECK(ac.entryMap.count("first")); CHECK(ac.entryMap.count("first"));
CHECK(ac.entryMap.count("second")); CHECK(ac.entryMap.count("second"));
CHECK_EQ(ac.context, AutocompleteContext::Property);
} }
TEST_CASE_FIXTURE(ACFixture, "autocomplete_documentation_symbols") 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("then") == 0);
CHECK(ac.entryMap.count("else") == 0); CHECK(ac.entryMap.count("else") == 0);
CHECK(ac.entryMap.count("elseif") == 0); CHECK(ac.entryMap.count("elseif") == 0);
CHECK_EQ(ac.context, AutocompleteContext::Expression);
ac = autocomplete('2'); ac = autocomplete('2');
CHECK(ac.entryMap.count("temp") == 0); 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("then"));
CHECK(ac.entryMap.count("else") == 0); CHECK(ac.entryMap.count("else") == 0);
CHECK(ac.entryMap.count("elseif") == 0); CHECK(ac.entryMap.count("elseif") == 0);
CHECK_EQ(ac.context, AutocompleteContext::Keyword);
ac = autocomplete('3'); ac = autocomplete('3');
CHECK(ac.entryMap.count("even")); CHECK(ac.entryMap.count("even"));
CHECK(ac.entryMap.count("then") == 0); CHECK(ac.entryMap.count("then") == 0);
CHECK(ac.entryMap.count("else") == 0); CHECK(ac.entryMap.count("else") == 0);
CHECK(ac.entryMap.count("elseif") == 0); CHECK(ac.entryMap.count("elseif") == 0);
CHECK_EQ(ac.context, AutocompleteContext::Expression);
ac = autocomplete('4'); ac = autocomplete('4');
CHECK(ac.entryMap.count("even") == 0); CHECK(ac.entryMap.count("even") == 0);
CHECK(ac.entryMap.count("then") == 0); CHECK(ac.entryMap.count("then") == 0);
CHECK(ac.entryMap.count("else")); CHECK(ac.entryMap.count("else"));
CHECK(ac.entryMap.count("elseif")); CHECK(ac.entryMap.count("elseif"));
CHECK_EQ(ac.context, AutocompleteContext::Keyword);
ac = autocomplete('5'); ac = autocomplete('5');
CHECK(ac.entryMap.count("temp")); 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("then") == 0);
CHECK(ac.entryMap.count("else") == 0); CHECK(ac.entryMap.count("else") == 0);
CHECK(ac.entryMap.count("elseif") == 0); CHECK(ac.entryMap.count("elseif") == 0);
CHECK_EQ(ac.context, AutocompleteContext::Expression);
ac = autocomplete('6'); ac = autocomplete('6');
CHECK(ac.entryMap.count("temp") == 0); 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("then"));
CHECK(ac.entryMap.count("else") == 0); CHECK(ac.entryMap.count("else") == 0);
CHECK(ac.entryMap.count("elseif") == 0); CHECK(ac.entryMap.count("elseif") == 0);
CHECK_EQ(ac.context, AutocompleteContext::Keyword);
ac = autocomplete('7'); ac = autocomplete('7');
CHECK(ac.entryMap.count("temp")); 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("then") == 0);
CHECK(ac.entryMap.count("else") == 0); CHECK(ac.entryMap.count("else") == 0);
CHECK(ac.entryMap.count("elseif") == 0); CHECK(ac.entryMap.count("elseif") == 0);
CHECK_EQ(ac.context, AutocompleteContext::Expression);
ac = autocomplete('8'); ac = autocomplete('8');
CHECK(ac.entryMap.count("even") == 0); CHECK(ac.entryMap.count("even") == 0);
CHECK(ac.entryMap.count("then") == 0); CHECK(ac.entryMap.count("then") == 0);
CHECK(ac.entryMap.count("else")); CHECK(ac.entryMap.count("else"));
CHECK(ac.entryMap.count("elseif")); CHECK(ac.entryMap.count("elseif"));
CHECK_EQ(ac.context, AutocompleteContext::Keyword);
ac = autocomplete('9'); ac = autocomplete('9');
CHECK(ac.entryMap.count("then") == 0); CHECK(ac.entryMap.count("then") == 0);
CHECK(ac.entryMap.count("else") == 0); CHECK(ac.entryMap.count("else") == 0);
CHECK(ac.entryMap.count("elseif") == 0); CHECK(ac.entryMap.count("elseif") == 0);
CHECK_EQ(ac.context, AutocompleteContext::Expression);
} }
TEST_CASE_FIXTURE(ACFixture, "autocomplete_if_else_regression") 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("number"));
CHECK(ac.entryMap.count("string")); CHECK(ac.entryMap.count("string"));
CHECK_EQ(ac.context, AutocompleteContext::Type);
} }
TEST_CASE_FIXTURE(ACFixture, "autocomplete_first_function_arg_expected_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("number"));
CHECK(ac.entryMap.count("string")); CHECK(ac.entryMap.count("string"));
CHECK_EQ(ac.context, AutocompleteContext::Type);
} }
TEST_CASE_FIXTURE(ACFixture, "autocomplete_default_type_pack_parameters") 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("number"));
CHECK(ac.entryMap.count("string")); CHECK(ac.entryMap.count("string"));
CHECK_EQ(ac.context, AutocompleteContext::Type);
} }
TEST_CASE_FIXTURE(ACBuiltinsFixture, "autocomplete_oop_implicit_self") 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("cat"));
CHECK(ac.entryMap.count("dog")); CHECK(ac.entryMap.count("dog"));
CHECK_EQ(ac.context, AutocompleteContext::String);
ac = autocomplete('2'); ac = autocomplete('2');
CHECK(ac.entryMap.count("\"cat\"")); CHECK(ac.entryMap.count("\"cat\""));
CHECK(ac.entryMap.count("\"dog\"")); CHECK(ac.entryMap.count("\"dog\""));
CHECK_EQ(ac.context, AutocompleteContext::Expression);
ac = autocomplete('3'); ac = autocomplete('3');
CHECK(ac.entryMap.count("cat")); CHECK(ac.entryMap.count("cat"));
CHECK(ac.entryMap.count("dog")); CHECK(ac.entryMap.count("dog"));
CHECK_EQ(ac.context, AutocompleteContext::String);
check(R"( check(R"(
type tagged = {tag:"cat", fieldx:number} | {tag:"dog", fieldy:number} 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("cat"));
CHECK(ac.entryMap.count("dog")); CHECK(ac.entryMap.count("dog"));
CHECK_EQ(ac.context, AutocompleteContext::String);
} }
TEST_CASE_FIXTURE(ACFixture, "autocomplete_string_singleton_equality") TEST_CASE_FIXTURE(ACFixture, "autocomplete_string_singleton_equality")
@ -2808,6 +2908,7 @@ f(@1)
CHECK(ac.entryMap["true"].typeCorrect == TypeCorrectKind::Correct); CHECK(ac.entryMap["true"].typeCorrect == TypeCorrectKind::Correct);
REQUIRE(ac.entryMap.count("false")); REQUIRE(ac.entryMap.count("false"));
CHECK(ac.entryMap["false"].typeCorrect == TypeCorrectKind::None); CHECK(ac.entryMap["false"].typeCorrect == TypeCorrectKind::None);
CHECK_EQ(ac.context, AutocompleteContext::Expression);
} }
TEST_CASE_FIXTURE(ACFixture, "autocomplete_string_singleton_escape") TEST_CASE_FIXTURE(ACFixture, "autocomplete_string_singleton_escape")
@ -3088,4 +3189,27 @@ a.@1
CHECK(ac.entryMap.count("y")); 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(); TEST_SUITE_END();

View file

@ -261,8 +261,6 @@ L1: RETURN R0 0
TEST_CASE("ForBytecode") TEST_CASE("ForBytecode")
{ {
ScopedFastFlag sff("LuauCompileNoIpairs", true);
// basic for loop: variable directly refers to internal iteration index (R2) // basic for loop: variable directly refers to internal iteration index (R2)
CHECK_EQ("\n" + compileFunction0("for i=1,5 do print(i) end"), R"( CHECK_EQ("\n" + compileFunction0("for i=1,5 do print(i) end"), R"(
LOADN R2 1 LOADN R2 1
@ -349,8 +347,6 @@ RETURN R0 0
TEST_CASE("ForBytecodeBuiltin") TEST_CASE("ForBytecodeBuiltin")
{ {
ScopedFastFlag sff("LuauCompileNoIpairs", true);
// we generally recognize builtins like pairs/ipairs and emit special opcodes // we generally recognize builtins like pairs/ipairs and emit special opcodes
CHECK_EQ("\n" + compileFunction0("for k,v in ipairs({}) do end"), R"( CHECK_EQ("\n" + compileFunction0("for k,v in ipairs({}) do end"), R"(
GETIMPORT R0 1 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"); 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") TEST_CASE("ArrayIndexLiteral")
@ -2163,8 +2222,6 @@ L1: RETURN R3 -1
TEST_CASE("UpvaluesLoopsBytecode") TEST_CASE("UpvaluesLoopsBytecode")
{ {
ScopedFastFlag sff("LuauCompileNoIpairs", true);
CHECK_EQ("\n" + compileFunction(R"( CHECK_EQ("\n" + compileFunction(R"(
function test() function test()
for i=1,10 do for i=1,10 do
@ -2780,46 +2837,44 @@ RETURN R0 0
TEST_CASE("AssignmentConflict") TEST_CASE("AssignmentConflict")
{ {
ScopedFastFlag sff("LuauCompileOptimalAssignment", true);
// assignments are left to right // assignments are left to right
CHECK_EQ("\n" + compileFunction0("local a, b a, b = 1, 2"), R"( CHECK_EQ("\n" + compileFunction0("local a, b a, b = 1, 2"), R"(
LOADNIL R0 LOADNIL R0
LOADNIL R1 LOADNIL R1
LOADN R2 1 LOADN R0 1
LOADN R3 2 LOADN R1 2
MOVE R0 R2
MOVE R1 R3
RETURN R0 0 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"( CHECK_EQ("\n" + compileFunction0("local a a, a[1] = 1, 2"), R"(
LOADNIL R0 LOADNIL R0
MOVE R1 R0 LOADN R1 1
LOADN R2 1 LOADN R2 2
LOADN R3 2 SETTABLEN R2 R0 1
MOVE R0 R2 MOVE R0 R1
SETTABLEN R3 R1 1
RETURN R0 0 RETURN R0 0
)"); )");
// note that this doesn't happen if the local assignment happens last naturally // 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"( CHECK_EQ("\n" + compileFunction0("local a a[1], a = 1, 2"), R"(
LOADNIL R0 LOADNIL R0
LOADN R1 1 LOADN R2 1
LOADN R2 2 LOADN R1 2
SETTABLEN R1 R0 1 SETTABLEN R2 R0 1
MOVE R0 R2 MOVE R0 R1
RETURN R0 0 RETURN R0 0
)"); )");
// this will happen if assigned register is used in any table expression, including as an object... // 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"( CHECK_EQ("\n" + compileFunction0("local a a, a.foo = 1, 2"), R"(
LOADNIL R0 LOADNIL R0
MOVE R1 R0 LOADN R1 1
LOADN R2 1 LOADN R2 2
LOADN R3 2 SETTABLEKS R2 R0 K0
MOVE R0 R2 MOVE R0 R1
SETTABLEKS R3 R1 K0
RETURN R0 0 RETURN R0 0
)"); )");
@ -2827,22 +2882,20 @@ RETURN R0 0
CHECK_EQ("\n" + compileFunction0("local a a, foo[a] = 1, 2"), R"( CHECK_EQ("\n" + compileFunction0("local a a, foo[a] = 1, 2"), R"(
LOADNIL R0 LOADNIL R0
GETIMPORT R1 1 GETIMPORT R1 1
MOVE R2 R0 LOADN R2 1
LOADN R3 1 LOADN R3 2
LOADN R4 2 SETTABLE R3 R1 R0
MOVE R0 R3 MOVE R0 R2
SETTABLE R4 R1 R2
RETURN R0 0 RETURN R0 0
)"); )");
// ... or both ... // ... or both ...
CHECK_EQ("\n" + compileFunction0("local a a, a[a] = 1, 2"), R"( CHECK_EQ("\n" + compileFunction0("local a a, a[a] = 1, 2"), R"(
LOADNIL R0 LOADNIL R0
MOVE R1 R0 LOADN R1 1
LOADN R2 1 LOADN R2 2
LOADN R3 2 SETTABLE R2 R0 R0
MOVE R0 R2 MOVE R0 R1
SETTABLE R3 R1 R1
RETURN R0 0 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"( CHECK_EQ("\n" + compileFunction0("local a, b a, b, a[b] = 1, 2, 3"), R"(
LOADNIL R0 LOADNIL R0
LOADNIL R1 LOADNIL R1
MOVE R2 R0 LOADN R2 1
MOVE R3 R1 LOADN R3 2
LOADN R4 1 LOADN R4 3
LOADN R5 2 SETTABLE R4 R0 R1
LOADN R6 3 MOVE R0 R2
MOVE R0 R4 MOVE R1 R3
MOVE R1 R5
SETTABLE R6 R2 R3
RETURN R0 0 RETURN R0 0
)"); )");
@ -2867,10 +2918,9 @@ RETURN R0 0
LOADNIL R0 LOADNIL R0
GETIMPORT R1 1 GETIMPORT R1 1
ADDK R2 R0 K2 ADDK R2 R0 K2
LOADN R3 1 LOADN R0 1
LOADN R4 2 LOADN R3 2
MOVE R0 R3 SETTABLE R3 R1 R2
SETTABLE R4 R1 R2
RETURN R0 0 RETURN R0 0
)"); )");
} }
@ -3849,8 +3899,6 @@ RETURN R0 1
TEST_CASE("SharedClosure") TEST_CASE("SharedClosure")
{ {
ScopedFastFlag sff("LuauCompileFreeReassign", true);
// closures can be shared even if functions refer to upvalues, as long as upvalues are top-level // closures can be shared even if functions refer to upvalues, as long as upvalues are top-level
CHECK_EQ("\n" + compileFunction(R"( CHECK_EQ("\n" + compileFunction(R"(
local val = ... local val = ...
@ -6063,6 +6111,8 @@ return
math.clamp(-1, 0, 1), math.clamp(-1, 0, 1),
math.sign(77), math.sign(77),
math.round(7.6), math.round(7.6),
bit32.extract(-1, 31),
bit32.replace(100, 1, 0),
(type("fin")) (type("fin"))
)", )",
0, 2), 0, 2),
@ -6114,8 +6164,10 @@ LOADK R43 K2
LOADN R44 0 LOADN R44 0
LOADN R45 1 LOADN R45 1
LOADN R46 8 LOADN R46 8
LOADK R47 K3 LOADN R47 1
RETURN R0 48 LOADN R48 101
LOADK R49 K3
RETURN R0 50
)"); )");
} }
@ -6126,7 +6178,8 @@ return
math.abs(), math.abs(),
math.max(1, true), math.max(1, true),
string.byte("abc", 42), string.byte("abc", 42),
bit32.rshift(10, 42) bit32.rshift(10, 42),
bit32.extract(1, 2, "3")
)", )",
0, 2), 0, 2),
R"( R"(
@ -6147,8 +6200,14 @@ L2: LOADN R4 10
FASTCALL2K 39 R4 K7 L3 FASTCALL2K 39 R4 K7 L3
LOADK R5 K7 LOADK R5 K7
GETIMPORT R3 13 GETIMPORT R3 13
CALL R3 2 -1 CALL R3 2 1
L3: RETURN R0 -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") TEST_CASE("LocalReassign")
{ {
ScopedFastFlag sff("LuauCompileFreeReassign", true);
// locals can be re-assigned and the register gets reused // locals can be re-assigned and the register gets reused
CHECK_EQ("\n" + compileFunction0(R"( CHECK_EQ("\n" + compileFunction0(R"(
local function test(a, b) 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(); TEST_SUITE_END();

View file

@ -724,6 +724,23 @@ TEST_CASE("Reference")
CHECK(dtorhits == 2); 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") TEST_CASE("ApiTables")
{ {
StateRef globalState(luaL_newstate(), lua_close); StateRef globalState(luaL_newstate(), lua_close);

View file

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

View file

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

View file

@ -1689,7 +1689,7 @@ TEST_CASE_FIXTURE(Fixture, "TestStringInterpolation")
REQUIRE_EQ(result.warnings.size(), 1); REQUIRE_EQ(result.warnings.size(), 1);
} }
TEST_CASE_FIXTURE(Fixture, "LintIntegerParsing") TEST_CASE_FIXTURE(Fixture, "IntegerParsing")
{ {
ScopedFastFlag luauLintParseIntegerIssues{"LuauLintParseIntegerIssues", true}; ScopedFastFlag luauLintParseIntegerIssues{"LuauLintParseIntegerIssues", true};
@ -1704,7 +1704,7 @@ local _ = 0x10000000000000000
} }
// TODO: remove with FFlagLuauErrorDoubleHexPrefix // TODO: remove with FFlagLuauErrorDoubleHexPrefix
TEST_CASE_FIXTURE(Fixture, "LintIntegerParsingDoublePrefix") TEST_CASE_FIXTURE(Fixture, "IntegerParsingDoublePrefix")
{ {
ScopedFastFlag luauLintParseIntegerIssues{"LuauLintParseIntegerIssues", true}; ScopedFastFlag luauLintParseIntegerIssues{"LuauLintParseIntegerIssues", true};
ScopedFastFlag luauErrorDoubleHexPrefix{"LuauErrorDoubleHexPrefix", false}; // Lint will be available until we start rejecting code 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"); "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(); TEST_SUITE_END();

View file

@ -317,4 +317,78 @@ type B = A
CHECK(toString(it->second.type) == "any"); 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(); TEST_SUITE_END();

View file

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

View file

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

View file

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

View file

@ -11,6 +11,33 @@ using namespace Luau;
TEST_SUITE_BEGIN("DefinitionTests"); 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") TEST_CASE_FIXTURE(Fixture, "definition_file_loading")
{ {
loadDefinition(R"( loadDefinition(R"(

View file

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

View file

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

View file

@ -329,6 +329,35 @@ function tbl:foo(b: number, c: number)
-- introduce BoundTypeVar to imported type -- introduce BoundTypeVar to imported type
arrayops.foo(self._regions) arrayops.foo(self._regions)
end 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) type Table = typeof(tbl)
)"); )");
@ -398,8 +427,6 @@ caused by:
TEST_CASE_FIXTURE(BuiltinsFixture, "constrained_anyification_clone_immutable_types") TEST_CASE_FIXTURE(BuiltinsFixture, "constrained_anyification_clone_immutable_types")
{ {
ScopedFastFlag luauAnyificationMustClone{"LuauAnyificationMustClone", true};
fileResolver.source["game/A"] = R"( fileResolver.source["game/A"] = R"(
return function(...) end return function(...) end
)"; )";

View file

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

View file

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

View file

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

View file

@ -17,7 +17,8 @@ struct TryUnifyFixture : Fixture
ScopePtr globalScope{new Scope{arena.addTypePack({TypeId{}})}}; ScopePtr globalScope{new Scope{arena.addTypePack({TypeId{}})}};
InternalErrorReporter iceHandler; InternalErrorReporter iceHandler;
UnifierSharedState unifierState{&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"); TEST_SUITE_BEGIN("TryUnifyTests");

View file

@ -182,6 +182,22 @@ TEST_CASE_FIXTURE(Fixture, "UnionTypeVarIterator_with_empty_union")
CHECK(actual.empty()); 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") TEST_CASE_FIXTURE(Fixture, "substitution_skip_failure")
{ {
TypeVar ftv11{FreeTypeVar{TypeLevel{}}}; TypeVar ftv11{FreeTypeVar{TypeLevel{}}};
@ -257,6 +273,7 @@ TEST_CASE_FIXTURE(Fixture, "substitution_skip_failure")
TypeId root = &ttvTweenResult; TypeId root = &ttvTweenResult;
typeChecker.currentModule = std::make_shared<Module>(); 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{}); 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() _G['bar'] = 1 return _G.bar end)() == 1)
assert((function() local a = 1 (function () a = 2 end)() return a end)() == 2) 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 -- upvalues
assert((function() local a = 1 function foo() return a end return foo() end)() == 1) 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(0xa0001111, 31, 1) == 1)
assert(bit32.extract(0x50000111, 31, 1) == 0) assert(bit32.extract(0x50000111, 31, 1) == 0)
assert(bit32.extract(0xf2345679, 0, 32) == 0xf2345679) 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, -1))
assert(not pcall(bit32.extract, 0, 32)) 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.btest("1", 3) == true)
assert(bit32.countlz("42") == 26) assert(bit32.countlz("42") == 26)
assert(bit32.countrz("42") == 1) assert(bit32.countrz("42") == 1)
assert(bit32.extract("42", 1, 3) == 5)
return('OK') return('OK')

View file

@ -295,7 +295,7 @@ end
-- testing syntax limits -- 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 function testrep (init, rep)
local s = "local a; "..init .. string.rep(rep, syntaxdepth) local s = "local a; "..init .. string.rep(rep, syntaxdepth)
local a,b = loadstring(s) 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("%*", "a\0b\0c") == "a\0b\0c")
assert(string.format("%*", string.rep("doge", 3000)) == string.rep("doge", 3000)) 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() assert(pcall(function()
string.format("%#*", "bad form") 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.builtin_types_are_not_exported
AnnotationTests.cannot_use_nonexported_type AnnotationTests.cannot_use_nonexported_type
AnnotationTests.cloned_interface_maintains_pointers_between_definitions AnnotationTests.cloned_interface_maintains_pointers_between_definitions
AnnotationTests.define_generic_type_alias
AnnotationTests.duplicate_type_param_name AnnotationTests.duplicate_type_param_name
AnnotationTests.for_loop_counter_annotation_is_checked AnnotationTests.for_loop_counter_annotation_is_checked
AnnotationTests.function_return_annotations_are_checked
AnnotationTests.generic_aliases_are_cloned_properly AnnotationTests.generic_aliases_are_cloned_properly
AnnotationTests.interface_types_belong_to_interface_arena AnnotationTests.interface_types_belong_to_interface_arena
AnnotationTests.luau_ice_triggers_an_ice 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_exception_with_flag_handler
AnnotationTests.luau_ice_triggers_an_ice_handler AnnotationTests.luau_ice_triggers_an_ice_handler
AnnotationTests.luau_print_is_magic_if_the_flag_is_set 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_intersection_typevar
AnnotationTests.occurs_check_on_cyclic_union_typevar AnnotationTests.occurs_check_on_cyclic_union_typevar
AnnotationTests.self_referential_type_alias
AnnotationTests.too_many_type_params AnnotationTests.too_many_type_params
AnnotationTests.two_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 AnnotationTests.use_type_required_from_another_file
AstQuery.last_argument_function_call_type AstQuery.last_argument_function_call_type
AstQuery::getDocumentationSymbolAtPosition.binding
AstQuery::getDocumentationSymbolAtPosition.event_callback_arg
AstQuery::getDocumentationSymbolAtPosition.overloaded_fn AstQuery::getDocumentationSymbolAtPosition.overloaded_fn
AstQuery::getDocumentationSymbolAtPosition.prop
AutocompleteTest.argument_types AutocompleteTest.argument_types
AutocompleteTest.arguments_to_global_lambda AutocompleteTest.arguments_to_global_lambda
AutocompleteTest.as_types
AutocompleteTest.autocomplete_boolean_singleton AutocompleteTest.autocomplete_boolean_singleton
AutocompleteTest.autocomplete_end_with_fn_exprs AutocompleteTest.autocomplete_end_with_fn_exprs
AutocompleteTest.autocomplete_end_with_lambda AutocompleteTest.autocomplete_end_with_lambda
AutocompleteTest.autocomplete_first_function_arg_expected_type AutocompleteTest.autocomplete_first_function_arg_expected_type
AutocompleteTest.autocomplete_for_in_middle_keywords AutocompleteTest.autocomplete_for_in_middle_keywords
AutocompleteTest.autocomplete_for_middle_keywords AutocompleteTest.autocomplete_for_middle_keywords
AutocompleteTest.autocomplete_if_else_regression
AutocompleteTest.autocomplete_if_middle_keywords AutocompleteTest.autocomplete_if_middle_keywords
AutocompleteTest.autocomplete_ifelse_expressions
AutocompleteTest.autocomplete_on_string_singletons AutocompleteTest.autocomplete_on_string_singletons
AutocompleteTest.autocomplete_oop_implicit_self AutocompleteTest.autocomplete_oop_implicit_self
AutocompleteTest.autocomplete_repeat_middle_keyword AutocompleteTest.autocomplete_repeat_middle_keyword
AutocompleteTest.autocomplete_string_singleton_equality AutocompleteTest.autocomplete_string_singleton_equality
AutocompleteTest.autocomplete_string_singleton_escape AutocompleteTest.autocomplete_string_singleton_escape
AutocompleteTest.autocomplete_string_singletons AutocompleteTest.autocomplete_string_singletons
AutocompleteTest.autocomplete_until_expression
AutocompleteTest.autocomplete_until_in_repeat
AutocompleteTest.autocomplete_while_middle_keywords AutocompleteTest.autocomplete_while_middle_keywords
AutocompleteTest.autocompleteProp_index_function_metamethod_is_variadic AutocompleteTest.autocompleteProp_index_function_metamethod_is_variadic
AutocompleteTest.bias_toward_inner_scope 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_function_params
AutocompleteTest.global_functions_are_not_scoped_lexically AutocompleteTest.global_functions_are_not_scoped_lexically
AutocompleteTest.if_then_else_elseif_completions AutocompleteTest.if_then_else_elseif_completions
AutocompleteTest.if_then_else_full_keywords
AutocompleteTest.keyword_methods AutocompleteTest.keyword_methods
AutocompleteTest.keyword_types AutocompleteTest.keyword_types
AutocompleteTest.library_non_self_calls_are_fine 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_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.assert_returns_false_and_string_iff_it_knows_the_first_argument_cannot_be_truthy
BuiltinTests.bad_select_should_not_crash BuiltinTests.bad_select_should_not_crash
BuiltinTests.builtin_tables_sealed
BuiltinTests.coroutine_resume_anything_goes BuiltinTests.coroutine_resume_anything_goes
BuiltinTests.coroutine_wrap_anything_goes BuiltinTests.coroutine_wrap_anything_goes
BuiltinTests.debug_info_is_crazy BuiltinTests.debug_info_is_crazy
@ -136,28 +116,20 @@ BuiltinTests.dont_add_definitions_to_persistent_types
BuiltinTests.find_capture_types BuiltinTests.find_capture_types
BuiltinTests.find_capture_types2 BuiltinTests.find_capture_types2
BuiltinTests.find_capture_types3 BuiltinTests.find_capture_types3
BuiltinTests.gcinfo
BuiltinTests.getfenv BuiltinTests.getfenv
BuiltinTests.global_singleton_types_are_sealed BuiltinTests.global_singleton_types_are_sealed
BuiltinTests.gmatch_capture_types BuiltinTests.gmatch_capture_types
BuiltinTests.gmatch_capture_types2 BuiltinTests.gmatch_capture_types2
BuiltinTests.gmatch_capture_types_balanced_escaped_parens BuiltinTests.gmatch_capture_types_balanced_escaped_parens
BuiltinTests.gmatch_capture_types_default_capture 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_parens_in_sets_are_ignored
BuiltinTests.gmatch_capture_types_set_containing_lbracket BuiltinTests.gmatch_capture_types_set_containing_lbracket
BuiltinTests.gmatch_definition BuiltinTests.gmatch_definition
BuiltinTests.ipairs_iterator_should_infer_types_and_type_check BuiltinTests.ipairs_iterator_should_infer_types_and_type_check
BuiltinTests.lua_51_exported_globals_all_exist
BuiltinTests.match_capture_types BuiltinTests.match_capture_types
BuiltinTests.match_capture_types2 BuiltinTests.match_capture_types2
BuiltinTests.math_max_checks_for_numbers 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.next_iterator_should_infer_types_and_type_check
BuiltinTests.no_persistent_typelevel_change
BuiltinTests.os_time_takes_optional_date_table BuiltinTests.os_time_takes_optional_date_table
BuiltinTests.pairs_iterator_should_infer_types_and_type_check BuiltinTests.pairs_iterator_should_infer_types_and_type_check
BuiltinTests.see_thru_select BuiltinTests.see_thru_select
@ -170,7 +142,6 @@ BuiltinTests.select_with_variadic_typepack_tail
BuiltinTests.select_with_variadic_typepack_tail_and_string_head BuiltinTests.select_with_variadic_typepack_tail_and_string_head
BuiltinTests.set_metatable_needs_arguments BuiltinTests.set_metatable_needs_arguments
BuiltinTests.setmetatable_should_not_mutate_persisted_types BuiltinTests.setmetatable_should_not_mutate_persisted_types
BuiltinTests.setmetatable_unpacks_arg_types_correctly
BuiltinTests.sort BuiltinTests.sort
BuiltinTests.sort_with_bad_predicate BuiltinTests.sort_with_bad_predicate
BuiltinTests.sort_with_predicate BuiltinTests.sort_with_predicate
@ -179,6 +150,8 @@ BuiltinTests.string_format_arg_types_inference
BuiltinTests.string_format_as_method BuiltinTests.string_format_as_method
BuiltinTests.string_format_correctly_ordered_types BuiltinTests.string_format_correctly_ordered_types
BuiltinTests.string_format_report_all_type_errors_at_correct_positions 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_argument
BuiltinTests.string_format_use_correct_argument2 BuiltinTests.string_format_use_correct_argument2
BuiltinTests.string_lib_self_noself 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
BuiltinTests.table_pack_reduce BuiltinTests.table_pack_reduce
BuiltinTests.table_pack_variadic BuiltinTests.table_pack_variadic
BuiltinTests.thread_is_a_type
BuiltinTests.tonumber_returns_optional_number_type BuiltinTests.tonumber_returns_optional_number_type
BuiltinTests.tonumber_returns_optional_number_type2 BuiltinTests.tonumber_returns_optional_number_type2
BuiltinTests.xpcall
DefinitionTests.class_definition_function_prop
DefinitionTests.declaring_generic_functions DefinitionTests.declaring_generic_functions
DefinitionTests.definition_file_class_function_args
DefinitionTests.definition_file_classes DefinitionTests.definition_file_classes
DefinitionTests.definition_file_loading DefinitionTests.definition_file_loading
DefinitionTests.definitions_documentation_symbols
DefinitionTests.documentation_symbols_dont_attach_to_persistent_types
DefinitionTests.single_class_type_identity_in_global_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.ast_node_at_position
FrontendTest.automatically_check_cyclically_dependent_scripts
FrontendTest.automatically_check_dependent_scripts FrontendTest.automatically_check_dependent_scripts
FrontendTest.check_without_builtin_next 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.dont_reparse_clean_file_when_linting
FrontendTest.environments FrontendTest.environments
FrontendTest.ignore_require_to_nonexistent_file
FrontendTest.imported_table_modification_2 FrontendTest.imported_table_modification_2
FrontendTest.it_should_be_safe_to_stringify_errors_when_full_type_graph_is_discarded FrontendTest.it_should_be_safe_to_stringify_errors_when_full_type_graph_is_discarded
FrontendTest.no_use_after_free_with_type_fun_instantiation FrontendTest.no_use_after_free_with_type_fun_instantiation
FrontendTest.nocheck_cycle_used_by_checked FrontendTest.nocheck_cycle_used_by_checked
FrontendTest.nocheck_modules_are_typed FrontendTest.nocheck_modules_are_typed
FrontendTest.produce_errors_for_unchanged_file_with_a_syntax_error 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.recheck_if_dependent_script_is_dirty
FrontendTest.reexport_cyclic_type FrontendTest.reexport_cyclic_type
FrontendTest.reexport_type_alias FrontendTest.reexport_type_alias
FrontendTest.report_require_to_nonexistent_file FrontendTest.report_require_to_nonexistent_file
FrontendTest.report_syntax_error_in_required_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 FrontendTest.trace_requires_in_nonstrict_mode
GenericsTests.apply_type_function_nested_generics1 GenericsTests.apply_type_function_nested_generics1
GenericsTests.apply_type_function_nested_generics2 GenericsTests.apply_type_function_nested_generics2
GenericsTests.better_mismatch_error_messages GenericsTests.better_mismatch_error_messages
GenericsTests.bound_tables_do_not_clone_original_fields GenericsTests.bound_tables_do_not_clone_original_fields
GenericsTests.calling_self_generic_methods
GenericsTests.check_generic_typepack_function GenericsTests.check_generic_typepack_function
GenericsTests.check_mutual_generic_functions GenericsTests.check_mutual_generic_functions
GenericsTests.correctly_instantiate_polymorphic_member_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_pack
GenericsTests.error_detailed_function_mismatch_generic_types GenericsTests.error_detailed_function_mismatch_generic_types
GenericsTests.factories_of_generics 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_few
GenericsTests.generic_argument_count_too_many GenericsTests.generic_argument_count_too_many
GenericsTests.generic_factories GenericsTests.generic_factories
@ -265,8 +219,7 @@ GenericsTests.generic_type_pack_unification3
GenericsTests.infer_generic_function_function_argument GenericsTests.infer_generic_function_function_argument
GenericsTests.infer_generic_function_function_argument_overloaded GenericsTests.infer_generic_function_function_argument_overloaded
GenericsTests.infer_generic_lib_function_function_argument GenericsTests.infer_generic_lib_function_function_argument
GenericsTests.infer_generic_property GenericsTests.infer_generic_methods
GenericsTests.inferred_local_vars_can_be_polytypes
GenericsTests.instantiate_cyclic_generic_function GenericsTests.instantiate_cyclic_generic_function
GenericsTests.instantiate_generic_function_in_assignments GenericsTests.instantiate_generic_function_in_assignments
GenericsTests.instantiate_generic_function_in_assignments2 GenericsTests.instantiate_generic_function_in_assignments2
@ -276,41 +229,32 @@ GenericsTests.local_vars_can_be_instantiated_polytypes
GenericsTests.mutable_state_polymorphism GenericsTests.mutable_state_polymorphism
GenericsTests.no_stack_overflow_from_quantifying GenericsTests.no_stack_overflow_from_quantifying
GenericsTests.properties_can_be_instantiated_polytypes GenericsTests.properties_can_be_instantiated_polytypes
GenericsTests.properties_can_be_polytypes
GenericsTests.rank_N_types_via_typeof GenericsTests.rank_N_types_via_typeof
GenericsTests.reject_clashing_generic_and_pack_names GenericsTests.reject_clashing_generic_and_pack_names
GenericsTests.self_recursive_instantiated_param GenericsTests.self_recursive_instantiated_param
GenericsTests.variadic_generics
IntersectionTypes.argument_is_intersection IntersectionTypes.argument_is_intersection
IntersectionTypes.error_detailed_intersection_all IntersectionTypes.error_detailed_intersection_all
IntersectionTypes.error_detailed_intersection_part IntersectionTypes.error_detailed_intersection_part
IntersectionTypes.fx_intersection_as_argument IntersectionTypes.fx_intersection_as_argument
IntersectionTypes.fx_union_as_argument_fails IntersectionTypes.fx_union_as_argument_fails
IntersectionTypes.index_on_an_intersection_type_with_mixed_types 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_with_property_guaranteed_to_exist
IntersectionTypes.index_on_an_intersection_type_works_at_arbitrary_depth IntersectionTypes.index_on_an_intersection_type_works_at_arbitrary_depth
IntersectionTypes.no_stack_overflow_from_flattenintersection IntersectionTypes.no_stack_overflow_from_flattenintersection
IntersectionTypes.overload_is_not_a_function IntersectionTypes.overload_is_not_a_function
IntersectionTypes.select_correct_union_fn IntersectionTypes.select_correct_union_fn
IntersectionTypes.should_still_pick_an_overload_whose_arguments_are_unions IntersectionTypes.should_still_pick_an_overload_whose_arguments_are_unions
IntersectionTypes.table_intersection_setmetatable
IntersectionTypes.table_intersection_write IntersectionTypes.table_intersection_write
IntersectionTypes.table_intersection_write_sealed IntersectionTypes.table_intersection_write_sealed
IntersectionTypes.table_intersection_write_sealed_indirect IntersectionTypes.table_intersection_write_sealed_indirect
IntersectionTypes.table_write_sealed_indirect IntersectionTypes.table_write_sealed_indirect
isSubtype.functions_and_any
isSubtype.intersection_of_functions_of_different_arities
isSubtype.intersection_of_tables isSubtype.intersection_of_tables
isSubtype.table_with_any_prop
isSubtype.table_with_table_prop isSubtype.table_with_table_prop
isSubtype.tables
Linter.DeprecatedApi
Linter.TableOperations Linter.TableOperations
ModuleTests.builtin_types_point_into_globalTypes_arena
ModuleTests.clone_self_property ModuleTests.clone_self_property
ModuleTests.deepClone_cyclic_table 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.delay_function_does_not_require_its_argument_to_return_anything
NonstrictModeTests.for_in_iterator_variables_are_any NonstrictModeTests.for_in_iterator_variables_are_any
NonstrictModeTests.function_parameters_are_any NonstrictModeTests.function_parameters_are_any
@ -333,8 +277,6 @@ Normalize.cyclic_intersection
Normalize.cyclic_table_normalizes_sensibly Normalize.cyclic_table_normalizes_sensibly
Normalize.cyclic_union Normalize.cyclic_union
Normalize.fuzz_failure_bound_type_is_normal_but_not_its_bounded_to 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_combine_on_bound_self
Normalize.intersection_inside_a_table_inside_another_intersection Normalize.intersection_inside_a_table_inside_another_intersection
Normalize.intersection_inside_a_table_inside_another_intersection_2 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_functions
Normalize.intersection_of_overlapping_tables Normalize.intersection_of_overlapping_tables
Normalize.intersection_of_tables_with_indexers Normalize.intersection_of_tables_with_indexers
Normalize.nested_table_normalization_with_non_table__no_ice
Normalize.normalization_does_not_convert_ever Normalize.normalization_does_not_convert_ever
Normalize.normalize_module_return_type Normalize.normalize_module_return_type
Normalize.normalize_unions_containing_never Normalize.normalize_unions_containing_never
Normalize.normalize_unions_containing_unknown 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.union_of_distinct_free_types
Normalize.variadic_tail_is_marked_normal Normalize.variadic_tail_is_marked_normal
Normalize.visiting_a_type_twice_is_not_considered_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.discriminate_from_x_not_equal_to_nil
ProvisionalTests.do_not_ice_when_trying_to_pick_first_of_generic_type_pack 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.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.function_returns_many_things_but_first_of_it_is_forgotten
ProvisionalTests.greedy_inference_with_shared_self_triggers_function_with_no_returns ProvisionalTests.greedy_inference_with_shared_self_triggers_function_with_no_returns
ProvisionalTests.invariant_table_properties_means_instantiating_tables_in_call_is_unsound 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.weird_fail_to_unify_type_pack
ProvisionalTests.weirditer_should_not_loop_forever ProvisionalTests.weirditer_should_not_loop_forever
ProvisionalTests.while_body_are_also_refined ProvisionalTests.while_body_are_also_refined
ProvisionalTests.xpcall_returns_what_f_returns
RefinementTest.and_constraint RefinementTest.and_constraint
RefinementTest.and_or_peephole_refinement RefinementTest.and_or_peephole_refinement
RefinementTest.apply_refinements_on_astexprindexexpr_whose_subscript_expr_is_constant_string 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.not_t_or_some_prop_of_t
RefinementTest.or_predicate_with_truthy_predicates RefinementTest.or_predicate_with_truthy_predicates
RefinementTest.parenthesized_expressions_are_followed_through 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_the_correct_types_opposite_of_when_a_is_not_number_or_string
RefinementTest.refine_unknowns RefinementTest.refine_unknowns
RefinementTest.string_not_equal_to_string_or_nil RefinementTest.string_not_equal_to_string_or_nil
@ -456,7 +392,6 @@ TableTests.augment_nested_table
TableTests.augment_table TableTests.augment_table
TableTests.builtin_table_names TableTests.builtin_table_names
TableTests.call_method TableTests.call_method
TableTests.call_method_with_explicit_self_argument
TableTests.cannot_augment_sealed_table TableTests.cannot_augment_sealed_table
TableTests.cannot_call_tables TableTests.cannot_call_tables
TableTests.cannot_change_type_of_unsealed_table_prop TableTests.cannot_change_type_of_unsealed_table_prop
@ -469,16 +404,13 @@ TableTests.common_table_element_union_in_call_tail
TableTests.confusing_indexing TableTests.confusing_indexing
TableTests.defining_a_method_for_a_builtin_sealed_table_must_fail 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_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_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_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_crash_when_setmetatable_does_not_produce_a_metatabletypevar
TableTests.dont_hang_when_trying_to_look_up_in_cyclic_metatable_index 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_invalidate_the_properties_iterator_of_free_table_when_rolled_back
TableTests.dont_leak_free_table_props TableTests.dont_leak_free_table_props
TableTests.dont_quantify_table_that_belongs_to_outer_scope 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.dont_suggest_exact_match_keys
TableTests.error_detailed_indexer_key TableTests.error_detailed_indexer_key
TableTests.error_detailed_indexer_value TableTests.error_detailed_indexer_value
@ -508,24 +440,19 @@ TableTests.infer_array_2
TableTests.infer_indexer_from_value_property_in_literal TableTests.infer_indexer_from_value_property_in_literal
TableTests.inferred_return_type_of_free_table TableTests.inferred_return_type_of_free_table
TableTests.inferring_crazy_table_should_also_be_quick TableTests.inferring_crazy_table_should_also_be_quick
TableTests.instantiate_table_cloning
TableTests.instantiate_table_cloning_2
TableTests.instantiate_table_cloning_3 TableTests.instantiate_table_cloning_3
TableTests.instantiate_tables_at_scope_level TableTests.instantiate_tables_at_scope_level
TableTests.invariant_table_properties_means_instantiating_tables_in_assignment_is_unsound
TableTests.leaking_bad_metatable_errors TableTests.leaking_bad_metatable_errors
TableTests.length_operator_intersection TableTests.length_operator_intersection
TableTests.length_operator_non_table_union TableTests.length_operator_non_table_union
TableTests.length_operator_union TableTests.length_operator_union
TableTests.length_operator_union_errors TableTests.length_operator_union_errors
TableTests.less_exponential_blowup_please
TableTests.meta_add TableTests.meta_add
TableTests.meta_add_both_ways TableTests.meta_add_both_ways
TableTests.meta_add_inferred TableTests.meta_add_inferred
TableTests.metatable_mismatch_should_fail TableTests.metatable_mismatch_should_fail
TableTests.missing_metatable_for_sealed_tables_do_not_get_inferred TableTests.missing_metatable_for_sealed_tables_do_not_get_inferred
TableTests.mixed_tables_with_implicit_numbered_keys TableTests.mixed_tables_with_implicit_numbered_keys
TableTests.MixedPropertiesAndIndexers
TableTests.nil_assign_doesnt_hit_indexer TableTests.nil_assign_doesnt_hit_indexer
TableTests.okay_to_add_property_to_unsealed_tables_by_function_call TableTests.okay_to_add_property_to_unsealed_tables_by_function_call
TableTests.only_ascribe_synthetic_names_at_module_scope 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
TableTests.pass_a_union_of_tables_to_a_function_that_requires_a_table_2 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.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.persistent_sealed_table_is_immutable
TableTests.prop_access_on_key_whose_types_mismatches
TableTests.property_lookup_through_tabletypevar_metatable TableTests.property_lookup_through_tabletypevar_metatable
TableTests.quantify_even_that_table_was_never_exported_at_all TableTests.quantify_even_that_table_was_never_exported_at_all
TableTests.quantify_metatables_of_metatables_of_table TableTests.quantify_metatables_of_metatables_of_table
@ -570,23 +497,16 @@ TableTests.tc_member_function_2
TableTests.top_table_type TableTests.top_table_type
TableTests.type_mismatch_on_massive_table_is_cut_short TableTests.type_mismatch_on_massive_table_is_cut_short
TableTests.unification_of_unions_in_a_self_referential_type TableTests.unification_of_unions_in_a_self_referential_type
TableTests.unifying_tables_shouldnt_uaf1
TableTests.unifying_tables_shouldnt_uaf2 TableTests.unifying_tables_shouldnt_uaf2
TableTests.used_colon_correctly
TableTests.used_colon_instead_of_dot TableTests.used_colon_instead_of_dot
TableTests.used_dot_instead_of_colon TableTests.used_dot_instead_of_colon
TableTests.used_dot_instead_of_colon_but_correctly
TableTests.width_subtyping TableTests.width_subtyping
ToDot.bound_table ToDot.bound_table
ToDot.class
ToDot.function ToDot.function
ToDot.metatable ToDot.metatable
ToDot.primitive
ToDot.table ToDot.table
ToString.exhaustive_toString_of_cyclic_table ToString.exhaustive_toString_of_cyclic_table
ToString.function_type_with_argument_names_and_self
ToString.function_type_with_argument_names_generic ToString.function_type_with_argument_names_generic
ToString.named_metatable_toStringNamedFunction
ToString.no_parentheses_around_cyclic_function_type_in_union ToString.no_parentheses_around_cyclic_function_type_in_union
ToString.toStringDetailed2 ToString.toStringDetailed2
ToString.toStringErrorPack ToString.toStringErrorPack
@ -605,13 +525,12 @@ TryUnifyTests.typepack_unification_should_trim_free_tails
TryUnifyTests.variadics_should_use_reversed_properly TryUnifyTests.variadics_should_use_reversed_properly
TypeAliases.cli_38393_recursive_intersection_oom TypeAliases.cli_38393_recursive_intersection_oom
TypeAliases.corecursive_types_generic TypeAliases.corecursive_types_generic
TypeAliases.do_not_quantify_unresolved_aliases
TypeAliases.forward_declared_alias_is_not_clobbered_by_prior_unification_with_any TypeAliases.forward_declared_alias_is_not_clobbered_by_prior_unification_with_any
TypeAliases.general_require_multi_assign TypeAliases.general_require_multi_assign
TypeAliases.generic_param_remap TypeAliases.generic_param_remap
TypeAliases.mismatched_generic_pack_type_param TypeAliases.mismatched_generic_pack_type_param
TypeAliases.mismatched_generic_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_1
TypeAliases.mutually_recursive_types_restriction_not_ok_2 TypeAliases.mutually_recursive_types_restriction_not_ok_2
TypeAliases.mutually_recursive_types_swapsies_not_ok TypeAliases.mutually_recursive_types_swapsies_not_ok
@ -619,29 +538,19 @@ TypeAliases.recursive_types_restriction_not_ok
TypeAliases.stringify_optional_parameterized_alias TypeAliases.stringify_optional_parameterized_alias
TypeAliases.stringify_type_alias_of_recursive_template_table_type TypeAliases.stringify_type_alias_of_recursive_template_table_type
TypeAliases.stringify_type_alias_of_recursive_template_table_type2 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_mutation
TypeAliases.type_alias_local_rename TypeAliases.type_alias_local_rename
TypeAliases.type_alias_of_an_imported_recursive_generic_type TypeAliases.type_alias_of_an_imported_recursive_generic_type
TypeAliases.type_alias_of_an_imported_recursive_type TypeAliases.type_alias_of_an_imported_recursive_type
TypeInfer.check_expr_recursion_limit
TypeInfer.checking_should_not_ice TypeInfer.checking_should_not_ice
TypeInfer.cli_50041_committing_txnlog_in_apollo_client_error
TypeInfer.cyclic_follow TypeInfer.cyclic_follow
TypeInfer.do_not_bind_a_free_table_to_a_union_containing_that_table TypeInfer.do_not_bind_a_free_table_to_a_union_containing_that_table
TypeInfer.dont_report_type_errors_within_an_AstStatError 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.globals
TypeInfer.globals2 TypeInfer.globals2
TypeInfer.index_expr_should_be_checked
TypeInfer.infer_assignment_value_types
TypeInfer.infer_assignment_value_types_mutable_lval 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.no_stack_overflow_from_isoptional
TypeInfer.recursive_metatable_crash
TypeInfer.tc_after_error_recovery_no_replacement_name_in_error TypeInfer.tc_after_error_recovery_no_replacement_name_in_error
TypeInfer.tc_if_else_expressions1 TypeInfer.tc_if_else_expressions1
TypeInfer.tc_if_else_expressions2 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_expected_type_3
TypeInfer.tc_if_else_expressions_type_union TypeInfer.tc_if_else_expressions_type_union
TypeInfer.type_infer_recursion_limit_no_ice TypeInfer.type_infer_recursion_limit_no_ice
TypeInfer.types stored in astResolvedTypes
TypeInfer.warn_on_lowercase_parent_property TypeInfer.warn_on_lowercase_parent_property
TypeInfer.weird_case
TypeInferAnyError.any_type_propagates
TypeInferAnyError.assign_prop_to_table_by_calling_any_yields_any 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_get_length_of_any
TypeInferAnyError.can_subscript_any
TypeInferAnyError.CheckMethodsOfAny
TypeInferAnyError.for_in_loop_iterator_is_any TypeInferAnyError.for_in_loop_iterator_is_any
TypeInferAnyError.for_in_loop_iterator_is_any2 TypeInferAnyError.for_in_loop_iterator_is_any2
TypeInferAnyError.for_in_loop_iterator_is_error TypeInferAnyError.for_in_loop_iterator_is_error
TypeInferAnyError.for_in_loop_iterator_is_error2 TypeInferAnyError.for_in_loop_iterator_is_error2
TypeInferAnyError.for_in_loop_iterator_returns_any TypeInferAnyError.for_in_loop_iterator_returns_any
TypeInferAnyError.for_in_loop_iterator_returns_any2 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.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.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_base_method
TypeInferClasses.call_instance_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_type_mismatch_with_name_conflict
TypeInferClasses.class_unification_type_mismatch_is_correct_order
TypeInferClasses.classes_can_have_overloaded_operators TypeInferClasses.classes_can_have_overloaded_operators
TypeInferClasses.classes_without_overloaded_operators_cannot_be_added TypeInferClasses.classes_without_overloaded_operators_cannot_be_added
TypeInferClasses.detailed_class_unification_error TypeInferClasses.detailed_class_unification_error
TypeInferClasses.function_arguments_are_covariant 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_type_is_not_contravariant
TypeInferClasses.higher_order_function_return_values_are_covariant TypeInferClasses.higher_order_function_return_values_are_covariant
TypeInferClasses.optional_class_field_access_error TypeInferClasses.optional_class_field_access_error
TypeInferClasses.table_class_unification_reports_sane_errors_for_missing_properties 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.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 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_indirect_function_case_where_it_is_ok_to_provide_too_many_arguments
TypeInferFunctions.another_recursive_local_function TypeInferFunctions.another_recursive_local_function
TypeInferFunctions.calling_function_with_anytypepack_doesnt_leak_free_types TypeInferFunctions.calling_function_with_anytypepack_doesnt_leak_free_types
TypeInferFunctions.calling_function_with_incorrect_argument_type_yields_errors_spanning_argument 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.complicated_return_types_require_an_explicit_annotation
TypeInferFunctions.cyclic_function_type_in_args TypeInferFunctions.cyclic_function_type_in_args
TypeInferFunctions.dont_give_other_overloads_message_if_only_one_argument_matching_overload_exists 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_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.duplicate_functions_with_different_signatures_not_allowed_in_nonstrict
TypeInferFunctions.error_detailed_function_mismatch_arg TypeInferFunctions.error_detailed_function_mismatch_arg
TypeInferFunctions.error_detailed_function_mismatch_arg_count TypeInferFunctions.error_detailed_function_mismatch_arg_count
TypeInferFunctions.error_detailed_function_mismatch_ret TypeInferFunctions.error_detailed_function_mismatch_ret
TypeInferFunctions.error_detailed_function_mismatch_ret_count TypeInferFunctions.error_detailed_function_mismatch_ret_count
TypeInferFunctions.error_detailed_function_mismatch_ret_mult TypeInferFunctions.error_detailed_function_mismatch_ret_mult
TypeInferFunctions.first_argument_can_be_optional
TypeInferFunctions.free_is_not_bound_to_unknown TypeInferFunctions.free_is_not_bound_to_unknown
TypeInferFunctions.func_expr_doesnt_leak_free TypeInferFunctions.func_expr_doesnt_leak_free
TypeInferFunctions.function_cast_error_uses_correct_language TypeInferFunctions.function_cast_error_uses_correct_language
TypeInferFunctions.function_decl_non_self_sealed_overwrite TypeInferFunctions.function_decl_non_self_sealed_overwrite
TypeInferFunctions.function_decl_non_self_sealed_overwrite_2 TypeInferFunctions.function_decl_non_self_sealed_overwrite_2
TypeInferFunctions.function_decl_non_self_unsealed_overwrite TypeInferFunctions.function_decl_non_self_unsealed_overwrite
TypeInferFunctions.function_decl_quantify_right_type
TypeInferFunctions.function_does_not_return_enough_values TypeInferFunctions.function_does_not_return_enough_values
TypeInferFunctions.function_statement_sealed_table_assignment_through_indexer TypeInferFunctions.function_statement_sealed_table_assignment_through_indexer
TypeInferFunctions.higher_order_function_2 TypeInferFunctions.higher_order_function_2
@ -734,16 +611,12 @@ TypeInferFunctions.ignored_return_values
TypeInferFunctions.inconsistent_higher_order_function TypeInferFunctions.inconsistent_higher_order_function
TypeInferFunctions.inconsistent_return_types TypeInferFunctions.inconsistent_return_types
TypeInferFunctions.infer_anonymous_function_arguments TypeInferFunctions.infer_anonymous_function_arguments
TypeInferFunctions.infer_anonymous_function_arguments_outside_call
TypeInferFunctions.infer_return_type_from_selected_overload TypeInferFunctions.infer_return_type_from_selected_overload
TypeInferFunctions.infer_that_function_does_not_return_a_table 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_not_to_supply_enough_retvals
TypeInferFunctions.it_is_ok_to_oversaturate_a_higher_order_function_argument TypeInferFunctions.it_is_ok_to_oversaturate_a_higher_order_function_argument
TypeInferFunctions.list_all_overloads_if_no_overload_takes_given_argument_count TypeInferFunctions.list_all_overloads_if_no_overload_takes_given_argument_count
TypeInferFunctions.list_only_alternative_overloads_that_match_argument_count TypeInferFunctions.list_only_alternative_overloads_that_match_argument_count
TypeInferFunctions.mutual_recursion
TypeInferFunctions.no_lossy_function_type TypeInferFunctions.no_lossy_function_type
TypeInferFunctions.occurs_check_failure_in_function_return_type TypeInferFunctions.occurs_check_failure_in_function_return_type
TypeInferFunctions.quantify_constrained_types TypeInferFunctions.quantify_constrained_types
@ -759,10 +632,8 @@ TypeInferFunctions.too_few_arguments_variadic_generic
TypeInferFunctions.too_few_arguments_variadic_generic2 TypeInferFunctions.too_few_arguments_variadic_generic2
TypeInferFunctions.too_many_arguments TypeInferFunctions.too_many_arguments
TypeInferFunctions.too_many_return_values TypeInferFunctions.too_many_return_values
TypeInferFunctions.toposort_doesnt_break_mutual_recursion
TypeInferFunctions.vararg_function_is_quantified TypeInferFunctions.vararg_function_is_quantified
TypeInferFunctions.vararg_functions_should_allow_calls_of_any_types_and_size TypeInferFunctions.vararg_functions_should_allow_calls_of_any_types_and_size
TypeInferLoops.correctly_scope_locals_while
TypeInferLoops.for_in_loop TypeInferLoops.for_in_loop
TypeInferLoops.for_in_loop_error_on_factory_not_returning_the_right_amount_of_values 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 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_iter_trailing_nil
TypeInferLoops.loop_typecheck_crash_on_empty_optional TypeInferLoops.loop_typecheck_crash_on_empty_optional
TypeInferLoops.properly_infer_iteratee_is_a_free_table 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.unreachable_code_after_infinite_loop
TypeInferLoops.varlist_declared_by_for_in_loop_should_be_free 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
TypeInferModules.do_not_modify_imported_types_2 TypeInferModules.do_not_modify_imported_types_2
TypeInferModules.do_not_modify_imported_types_3 TypeInferModules.do_not_modify_imported_types_3
TypeInferModules.do_not_modify_imported_types_4
TypeInferModules.general_require_call_expression TypeInferModules.general_require_call_expression
TypeInferModules.general_require_type_mismatch TypeInferModules.general_require_type_mismatch
TypeInferModules.module_type_conflict TypeInferModules.module_type_conflict
@ -808,16 +671,14 @@ TypeInferModules.require_module_that_does_not_export
TypeInferModules.require_types TypeInferModules.require_types
TypeInferModules.type_error_of_unknown_qualified_type TypeInferModules.type_error_of_unknown_qualified_type
TypeInferModules.warn_if_you_try_to_require_a_non_modulescript 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_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_it_wont_help_2
TypeInferOOP.dont_suggest_using_colon_rather_than_dot_if_not_defined_with_colon 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.inferred_methods_of_free_tables_have_the_same_level_as_the_enclosing_table
TypeInferOOP.inferring_hundreds_of_self_calls_should_not_suffocate_memory TypeInferOOP.inferring_hundreds_of_self_calls_should_not_suffocate_memory
TypeInferOOP.method_depends_on_table
TypeInferOOP.methods_are_topologically_sorted TypeInferOOP.methods_are_topologically_sorted
TypeInferOOP.nonstrict_self_mismatch_tail 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
TypeInferOperators.and_adds_boolean_no_superfluous_union TypeInferOperators.and_adds_boolean_no_superfluous_union
TypeInferOperators.and_binexps_dont_unify 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.cli_38355_recursive_union
TypeInferOperators.compare_numbers TypeInferOperators.compare_numbers
TypeInferOperators.compare_strings TypeInferOperators.compare_strings
TypeInferOperators.compound_assign_basic
TypeInferOperators.compound_assign_metatable
TypeInferOperators.compound_assign_mismatch_metatable TypeInferOperators.compound_assign_mismatch_metatable
TypeInferOperators.compound_assign_mismatch_op TypeInferOperators.compound_assign_mismatch_op
TypeInferOperators.compound_assign_mismatch_result TypeInferOperators.compound_assign_mismatch_result
@ -872,37 +731,25 @@ TypeInferPrimitives.string_function_other
TypeInferPrimitives.string_index TypeInferPrimitives.string_index
TypeInferPrimitives.string_length TypeInferPrimitives.string_length
TypeInferPrimitives.string_method TypeInferPrimitives.string_method
TypeInferUnknownNever.array_like_table_of_never_is_inhabitable
TypeInferUnknownNever.assign_to_global_which_is_never TypeInferUnknownNever.assign_to_global_which_is_never
TypeInferUnknownNever.assign_to_local_which_is_never TypeInferUnknownNever.assign_to_local_which_is_never
TypeInferUnknownNever.assign_to_prop_which_is_never TypeInferUnknownNever.assign_to_prop_which_is_never
TypeInferUnknownNever.assign_to_subscript_which_is_never TypeInferUnknownNever.assign_to_subscript_which_is_never
TypeInferUnknownNever.call_never TypeInferUnknownNever.call_never
TypeInferUnknownNever.dont_unify_operands_if_one_of_the_operand_is_never_in_any_ordering_operators 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_never
TypeInferUnknownNever.index_on_union_of_tables_for_properties_that_is_sorta_never TypeInferUnknownNever.index_on_union_of_tables_for_properties_that_is_sorta_never
TypeInferUnknownNever.length_of_never TypeInferUnknownNever.length_of_never
TypeInferUnknownNever.math_operators_and_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_uninhabitable
TypeInferUnknownNever.type_packs_containing_never_is_itself_uninhabitable2 TypeInferUnknownNever.type_packs_containing_never_is_itself_uninhabitable2
TypeInferUnknownNever.unary_minus_of_never TypeInferUnknownNever.unary_minus_of_never
TypeInferUnknownNever.unknown_is_reflexive TypeInferUnknownNever.unknown_is_reflexive
TypeInferUnknownNever.unknown_subtype_and_string_supertype
TypePackTests.cyclic_type_packs
TypePackTests.higher_order_function TypePackTests.higher_order_function
TypePackTests.multiple_varargs_inference_are_not_confused TypePackTests.multiple_varargs_inference_are_not_confused
TypePackTests.no_return_size_should_be_zero TypePackTests.no_return_size_should_be_zero
TypePackTests.pack_tail_unification_check TypePackTests.pack_tail_unification_check
TypePackTests.parenthesized_varargs_returns_any TypePackTests.parenthesized_varargs_returns_any
TypePackTests.self_and_varargs_should_work
TypePackTests.type_alias_backwards_compatible TypePackTests.type_alias_backwards_compatible
TypePackTests.type_alias_default_export TypePackTests.type_alias_default_export
TypePackTests.type_alias_default_mixed_self 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_default_type_self
TypePackTests.type_alias_defaults_confusing_types TypePackTests.type_alias_defaults_confusing_types
TypePackTests.type_alias_defaults_recursive_type 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_multi
TypePackTests.type_alias_type_pack_variadic TypePackTests.type_alias_type_pack_variadic
TypePackTests.type_alias_type_packs TypePackTests.type_alias_type_packs
@ -949,6 +794,7 @@ TypeSingletons.string_singletons_escape_chars
TypeSingletons.string_singletons_mismatch TypeSingletons.string_singletons_mismatch
TypeSingletons.table_insert_with_a_singleton_argument TypeSingletons.table_insert_with_a_singleton_argument
TypeSingletons.table_properties_type_error_escapes TypeSingletons.table_properties_type_error_escapes
TypeSingletons.tagged_unions_immutable_tag
TypeSingletons.tagged_unions_using_singletons TypeSingletons.tagged_unions_using_singletons
TypeSingletons.taking_the_length_of_string_singleton TypeSingletons.taking_the_length_of_string_singleton
TypeSingletons.taking_the_length_of_union_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.optional_union_methods
UnionTypes.return_types_can_be_disjoint UnionTypes.return_types_can_be_disjoint
UnionTypes.table_union_write_indirect UnionTypes.table_union_write_indirect
UnionTypes.unify_unsealed_table_union_check
UnionTypes.union_equality_comparisons UnionTypes.union_equality_comparisons

View file

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

View file

@ -15,12 +15,20 @@ def updatesize(d, k, s):
def sortedsize(p): def sortedsize(p):
return sorted(p, key = lambda s: s[1][1], reverse = True) 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: with open(sys.argv[1]) as f:
dump = json.load(f) dump = json.load(f)
heap = dump["objects"] 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_type = {}
size_udata = {} size_udata = {}
size_category = {} size_category = {}
@ -33,11 +41,7 @@ for addr, obj in heap.items():
if obj["type"] == "userdata" and "metatable" in obj: if obj["type"] == "userdata" and "metatable" in obj:
metatable = heap[obj["metatable"]] metatable = heap[obj["metatable"]]
pairs = metatable.get("pairs", []) typemt = getkey(heap, metatable, "__type") or "unknown"
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"]
updatesize(size_udata, typemt, obj["size"]) updatesize(size_udata, typemt, obj["size"])
print("objects by type:") print("objects by type:")
@ -55,5 +59,6 @@ if len(size_category) != 0:
print("objects by category:") print("objects by category:")
for type, (count, size) in sortedsize(size_category.items()): 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") 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 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(",") source, function, line = key.split(",")
node.function = function node.function = function
node.source = source node.source = source
node.line = int(line) if len(line) > 0 else 0 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(): if 'Children' in obj:
nodeFromJSONbject(node.child(key), key, obj) for key, obj in obj['Children'].items():
nodeFromJSONObject(node.child(key), key, obj)
return node return node
@ -77,8 +87,9 @@ def nodeFromJSONFile(source_file):
root = Node() root = Node()
for key, obj in dump['Children'].items(): if 'Children' in dump:
nodeFromJSONbject(root.child(key), key, obj) for key, obj in dump['Children'].items():
nodeFromJSONObject(root.child(key), key, obj)
return root return root