Sync to upstream/release/588 (#992)

Type checker/autocomplete:
* `Luau::autocomplete` no longer performs typechecking internally, make
sure to run `Frontend::check` before performing autocomplete requests
* Autocomplete string suggestions without "" are now only suggested
inside the ""
* Autocomplete suggestions now include `function (anonymous autofilled)`
key with a full suggestion for the function expression (with arguments
included) stored in `AutocompleteEntry::insertText`
* `AutocompleteEntry::indexedWithSelf` is provided for function call
suggestions made with `:`
* Cyclic modules now see each other type exports as `any` to prevent
memory use-after-free (similar to module return type)

Runtime:
* Updated inline/loop unroll cost model to better handle assignments
(Fixes https://github.com/Roblox/luau/issues/978)
* `math.noise` speed was improved by ~30%
* `table.concat` speed was improved by ~5-7%
* `tonumber` and `tostring` now have fastcall paths that execute ~1.5x
and ~2.5x faster respectively (fixes #777)
* Fixed crash in `luaL_typename` when index refers to a non-existing
value
* Fixed potential out of memory scenario when using `string.sub` or
`string.char` in a loop
* Fixed behavior of some fastcall builtins when called without arguments
under -O2 to match original functions
* Support for native code execution in VM is now enabled by default
(note: native code still has to be generated explicitly)
* `Codegen::compile` now accepts `CodeGen_OnlyNativeModules` flag. When
set, only modules that have a `--!native` hot-comment at the top will be
compiled to native code

In our new typechecker:
* Generic type packs are no longer considered to be variadic during
unification
* Timeout and cancellation now works in new solver
* Fixed false positive errors around 'table' and 'function' type
refinements
* Table literals now use covariant unification rules. This is sound
since literal has no type specified and has no aliases
* Fixed issues with blocked types escaping the constraint solver
* Fixed more places where error messages that should've been suppressed
were still reported
* Fixed errors when iterating over a top table type

In our native code generation (jit):
* 'DebugLuauAbortingChecks' flag is now supported on A64
* LOP_NEWCLOSURE has been translated to IR
This commit is contained in:
vegorov-rbx 2023-07-28 08:13:53 -07:00 committed by GitHub
parent 087be529b0
commit 76f67e0733
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
96 changed files with 3057 additions and 991 deletions

View file

@ -38,6 +38,7 @@ enum class AutocompleteEntryKind
String,
Type,
Module,
GeneratedFunction,
};
enum class ParenthesesRecommendation
@ -70,6 +71,10 @@ struct AutocompleteEntry
std::optional<std::string> documentationSymbol = std::nullopt;
Tags tags;
ParenthesesRecommendation parens = ParenthesesRecommendation::None;
std::optional<std::string> insertText;
// Only meaningful if kind is Property.
bool indexedWithSelf = false;
};
using AutocompleteEntryMap = std::unordered_map<std::string, AutocompleteEntry>;
@ -94,4 +99,6 @@ using StringCompletionCallback =
AutocompleteResult autocomplete(Frontend& frontend, const ModuleName& moduleName, Position position, StringCompletionCallback callback);
constexpr char kGeneratedAnonymousFunctionEntryName[] = "function (anonymous autofilled)";
} // namespace Luau

View file

@ -19,7 +19,7 @@ struct Config
{
Config();
Mode mode;
Mode mode = Mode::Nonstrict;
ParseOptions parseOptions;

View file

@ -94,12 +94,13 @@ struct ConstraintGraphBuilder
ScopePtr globalScope;
std::function<void(const ModuleName&, const ScopePtr&)> prepareModuleScope;
std::vector<RequireCycle> requireCycles;
DcrLogger* logger;
ConstraintGraphBuilder(ModulePtr module, TypeArena* arena, NotNull<ModuleResolver> moduleResolver, NotNull<BuiltinTypes> builtinTypes,
NotNull<InternalErrorReporter> ice, const ScopePtr& globalScope, std::function<void(const ModuleName&, const ScopePtr&)> prepareModuleScope,
DcrLogger* logger, NotNull<DataFlowGraph> dfg);
DcrLogger* logger, NotNull<DataFlowGraph> dfg, std::vector<RequireCycle> requireCycles);
/**
* Fabricates a new free type belonging to a given scope.

View file

@ -8,6 +8,7 @@
#include "Luau/Normalize.h"
#include "Luau/ToString.h"
#include "Luau/Type.h"
#include "Luau/TypeCheckLimits.h"
#include "Luau/Variant.h"
#include <vector>
@ -81,9 +82,11 @@ struct ConstraintSolver
std::vector<RequireCycle> requireCycles;
DcrLogger* logger;
TypeCheckLimits limits;
explicit ConstraintSolver(NotNull<Normalizer> normalizer, NotNull<Scope> rootScope, std::vector<NotNull<Constraint>> constraints,
ModuleName moduleName, NotNull<ModuleResolver> moduleResolver, std::vector<RequireCycle> requireCycles, DcrLogger* logger);
ModuleName moduleName, NotNull<ModuleResolver> moduleResolver, std::vector<RequireCycle> requireCycles, DcrLogger* logger,
TypeCheckLimits limits);
// Randomize the order in which to dispatch constraints
void randomize(unsigned seed);
@ -280,6 +283,9 @@ private:
TypePackId anyifyModuleReturnTypePackGenerics(TypePackId tp);
void throwTimeLimitError();
void throwUserCancelError();
ToStringOptions opts;
};

View file

@ -1,9 +1,11 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#pragma once
#include "Luau/DenseHash.h"
#include "Luau/Type.h"
#include <optional>
#include <string>
#include <unordered_map>
namespace Luau
{
@ -17,6 +19,7 @@ struct DiffPathNode
FunctionReturn,
Union,
Intersection,
Negation,
};
Kind kind;
// non-null when TableProperty
@ -54,11 +57,15 @@ struct DiffPathNodeLeaf
std::optional<Name> tableProperty;
std::optional<int> minLength;
bool isVariadic;
DiffPathNodeLeaf(std::optional<TypeId> ty, std::optional<Name> tableProperty, std::optional<int> minLength, bool isVariadic)
// TODO: Rename to anonymousIndex, for both union and Intersection
std::optional<size_t> unionIndex;
DiffPathNodeLeaf(
std::optional<TypeId> ty, std::optional<Name> tableProperty, std::optional<int> minLength, bool isVariadic, std::optional<size_t> unionIndex)
: ty(ty)
, tableProperty(tableProperty)
, minLength(minLength)
, isVariadic(isVariadic)
, unionIndex(unionIndex)
{
}
@ -66,6 +73,8 @@ struct DiffPathNodeLeaf
static DiffPathNodeLeaf detailsTableProperty(TypeId ty, Name tableProperty);
static DiffPathNodeLeaf detailsUnionIndex(TypeId ty, size_t index);
static DiffPathNodeLeaf detailsLength(int minLength, bool isVariadic);
static DiffPathNodeLeaf nullopts();
@ -82,11 +91,12 @@ struct DiffError
enum Kind
{
Normal,
MissingProperty,
MissingTableProperty,
MissingUnionMember,
MissingIntersectionMember,
IncompatibleGeneric,
LengthMismatchInFnArgs,
LengthMismatchInFnRets,
LengthMismatchInUnion,
LengthMismatchInIntersection,
};
Kind kind;
@ -141,6 +151,8 @@ struct DifferEnvironment
{
TypeId rootLeft;
TypeId rootRight;
DenseHashMap<TypeId, TypeId> genericMatchedPairs;
};
DifferResult diff(TypeId ty1, TypeId ty2);

View file

@ -6,6 +6,7 @@
#include "Luau/ModuleResolver.h"
#include "Luau/RequireTracer.h"
#include "Luau/Scope.h"
#include "Luau/TypeCheckLimits.h"
#include "Luau/TypeInfer.h"
#include "Luau/Variant.h"
@ -189,14 +190,6 @@ struct Frontend
std::optional<CheckResult> getCheckResult(const ModuleName& name, bool accumulateNested, bool forAutocomplete = false);
private:
struct TypeCheckLimits
{
std::optional<double> finishTime;
std::optional<int> instantiationChildLimit;
std::optional<int> unifierIterationLimit;
std::shared_ptr<FrontendCancellationToken> cancellationToken;
};
ModulePtr check(const SourceModule& sourceModule, Mode mode, std::vector<RequireCycle> requireCycles, std::optional<ScopePtr> environmentScope,
bool forAutocomplete, bool recordJsonLog, TypeCheckLimits typeCheckLimits);
@ -248,11 +241,12 @@ public:
ModulePtr check(const SourceModule& sourceModule, const std::vector<RequireCycle>& requireCycles, NotNull<BuiltinTypes> builtinTypes,
NotNull<InternalErrorReporter> iceHandler, NotNull<ModuleResolver> moduleResolver, NotNull<FileResolver> fileResolver,
const ScopePtr& globalScope, std::function<void(const ModuleName&, const ScopePtr&)> prepareModuleScope, FrontendOptions options);
const ScopePtr& globalScope, std::function<void(const ModuleName&, const ScopePtr&)> prepareModuleScope, FrontendOptions options,
TypeCheckLimits limits);
ModulePtr check(const SourceModule& sourceModule, const std::vector<RequireCycle>& requireCycles, NotNull<BuiltinTypes> builtinTypes,
NotNull<InternalErrorReporter> iceHandler, NotNull<ModuleResolver> moduleResolver, NotNull<FileResolver> fileResolver,
const ScopePtr& globalScope, std::function<void(const ModuleName&, const ScopePtr&)> prepareModuleScope, FrontendOptions options,
bool recordJsonLog);
TypeCheckLimits limits, bool recordJsonLog);
} // namespace Luau

View file

@ -277,6 +277,9 @@ struct NormalizedType
/// Returns true if this type should result in error suppressing behavior.
bool shouldSuppressErrors() const;
/// Returns true if this type contains the primitve top table type, `table`.
bool hasTopTable() const;
// Helpers that improve readability of the above (they just say if the component is present)
bool hasTops() const;
bool hasBooleans() const;

View file

@ -6,8 +6,6 @@
#include <string>
LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution)
namespace Luau
{
@ -42,17 +40,7 @@ struct Symbol
return local != nullptr || global.value != nullptr;
}
bool operator==(const Symbol& rhs) const
{
if (local)
return local == rhs.local;
else if (global.value)
return rhs.global.value && global == rhs.global.value; // Subtlety: AstName::operator==(const char*) uses strcmp, not pointer identity.
else if (FFlag::DebugLuauDeferredConstraintResolution)
return !rhs.local && !rhs.global.value; // Reflexivity: we already know `this` Symbol is empty, so check that rhs is.
else
return false;
}
bool operator==(const Symbol& rhs) const;
bool operator!=(const Symbol& rhs) const
{

View file

@ -9,12 +9,16 @@
namespace Luau
{
struct Module;
struct TypeArena
{
TypedAllocator<Type> types;
TypedAllocator<TypePackVar> typePacks;
// Owning module, if any
Module* owningModule = nullptr;
void clear();
template<typename T>

View file

@ -0,0 +1,41 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#pragma once
#include "Luau/Cancellation.h"
#include "Luau/Error.h"
#include <memory>
#include <optional>
#include <string>
namespace Luau
{
class TimeLimitError : public InternalCompilerError
{
public:
explicit TimeLimitError(const std::string& moduleName)
: InternalCompilerError("Typeinfer failed to complete in allotted time", moduleName)
{
}
};
class UserCancelError : public InternalCompilerError
{
public:
explicit UserCancelError(const std::string& moduleName)
: InternalCompilerError("Analysis has been cancelled by user", moduleName)
{
}
};
struct TypeCheckLimits
{
std::optional<double> finishTime;
std::optional<int> instantiationChildLimit;
std::optional<int> unifierIterationLimit;
std::shared_ptr<FrontendCancellationToken> cancellationToken;
};
} // namespace Luau

View file

@ -10,6 +10,7 @@
#include "Luau/Symbol.h"
#include "Luau/TxnLog.h"
#include "Luau/Type.h"
#include "Luau/TypeCheckLimits.h"
#include "Luau/TypePack.h"
#include "Luau/TypeUtils.h"
#include "Luau/Unifier.h"
@ -56,24 +57,6 @@ struct HashBoolNamePair
size_t operator()(const std::pair<bool, Name>& pair) const;
};
class TimeLimitError : public InternalCompilerError
{
public:
explicit TimeLimitError(const std::string& moduleName)
: InternalCompilerError("Typeinfer failed to complete in allotted time", moduleName)
{
}
};
class UserCancelError : public InternalCompilerError
{
public:
explicit UserCancelError(const std::string& moduleName)
: InternalCompilerError("Analysis has been cancelled by user", moduleName)
{
}
};
struct GlobalTypes
{
GlobalTypes(NotNull<BuiltinTypes> builtinTypes);

View file

@ -14,6 +14,7 @@ namespace Luau
struct TxnLog;
struct TypeArena;
class Normalizer;
enum class ValueContext
{
@ -55,6 +56,51 @@ std::vector<TypeId> reduceUnion(const std::vector<TypeId>& types);
*/
TypeId stripNil(NotNull<BuiltinTypes> builtinTypes, TypeArena& arena, TypeId ty);
enum class ErrorSuppression
{
Suppress,
DoNotSuppress,
NormalizationFailed
};
/**
* Normalizes the given type using the normalizer to determine if the type
* should suppress any errors that would be reported involving it.
* @param normalizer the normalizer to use
* @param ty the type to check for error suppression
* @returns an enum indicating whether or not to suppress the error or to signal a normalization failure
*/
ErrorSuppression shouldSuppressErrors(NotNull<Normalizer> normalizer, TypeId ty);
/**
* Flattens and normalizes the given typepack using the normalizer to determine if the type
* should suppress any errors that would be reported involving it.
* @param normalizer the normalizer to use
* @param tp the typepack to check for error suppression
* @returns an enum indicating whether or not to suppress the error or to signal a normalization failure
*/
ErrorSuppression shouldSuppressErrors(NotNull<Normalizer> normalizer, TypePackId tp);
/**
* Normalizes the two given type using the normalizer to determine if either type
* should suppress any errors that would be reported involving it.
* @param normalizer the normalizer to use
* @param ty1 the first type to check for error suppression
* @param ty2 the second type to check for error suppression
* @returns an enum indicating whether or not to suppress the error or to signal a normalization failure
*/
ErrorSuppression shouldSuppressErrors(NotNull<Normalizer> normalizer, TypeId ty1, TypeId ty2);
/**
* Flattens and normalizes the two given typepacks using the normalizer to determine if either type
* should suppress any errors that would be reported involving it.
* @param normalizer the normalizer to use
* @param tp1 the first typepack to check for error suppression
* @param tp2 the second typepack to check for error suppression
* @returns an enum indicating whether or not to suppress the error or to signal a normalization failure
*/
ErrorSuppression shouldSuppressErrors(NotNull<Normalizer> normalizer, TypePackId tp1, TypePackId tp2);
template<typename T, typename Ty>
const T* get(std::optional<Ty> ty)
{

View file

@ -43,6 +43,21 @@ struct Widen : Substitution
TypePackId operator()(TypePackId ty);
};
/**
* Normally, when we unify table properties, we must do so invariantly, but we
* can introduce a special exception: If the table property in the subtype
* position arises from a literal expression, it is safe to instead perform a
* covariant check.
*
* This is very useful for typechecking cases where table literals (and trees of
* table literals) are passed directly to functions.
*
* In this case, we know that the property has no other name referring to it and
* so it is perfectly safe for the function to mutate the table any way it
* wishes.
*/
using LiteralProperties = DenseHashSet<Name>;
// TODO: Use this more widely.
struct UnifierOptions
{
@ -80,7 +95,7 @@ struct Unifier
// Configure the Unifier to test for scope subsumption via embedded Scope
// pointers rather than TypeLevels.
void enableScopeTests();
void enableNewSolver();
// Test whether the two type vars unify. Never commits the result.
ErrorVec canUnify(TypeId subTy, TypeId superTy);
@ -90,10 +105,10 @@ struct Unifier
* Populate the vector errors with any type errors that may arise.
* Populate the transaction log with the set of TypeIds that need to be reset to undo the unification attempt.
*/
void tryUnify(TypeId subTy, TypeId superTy, bool isFunctionCall = false, bool isIntersection = false);
void tryUnify(TypeId subTy, TypeId superTy, bool isFunctionCall = false, bool isIntersection = false, const LiteralProperties* aliasableMap = nullptr);
private:
void tryUnify_(TypeId subTy, TypeId superTy, bool isFunctionCall = false, bool isIntersection = false);
void tryUnify_(TypeId subTy, TypeId superTy, bool isFunctionCall = false, bool isIntersection = false, const LiteralProperties* aliasableMap = nullptr);
void tryUnifyUnionWithType(TypeId subTy, const UnionType* uv, TypeId superTy);
// Traverse the two types provided and block on any BlockedTypes we find.
@ -108,7 +123,7 @@ private:
void tryUnifyPrimitives(TypeId subTy, TypeId superTy);
void tryUnifySingletons(TypeId subTy, TypeId superTy);
void tryUnifyFunctions(TypeId subTy, TypeId superTy, bool isFunctionCall = false);
void tryUnifyTables(TypeId subTy, TypeId superTy, bool isIntersection = false);
void tryUnifyTables(TypeId subTy, TypeId superTy, bool isIntersection = false, const LiteralProperties* aliasableMap = nullptr);
void tryUnifyScalarShape(TypeId subTy, TypeId superTy, bool reversed);
void tryUnifyWithMetatable(TypeId subTy, TypeId superTy, bool reversed);
void tryUnifyWithClass(TypeId subTy, TypeId superTy, bool reversed);
@ -163,8 +178,10 @@ private:
// Available after regular type pack unification errors
std::optional<int> firstPackErrorPos;
// If true, we use the scope hierarchy rather than TypeLevels
bool useScopes = false;
// If true, we do a bunch of small things differently to work better with
// the new type inference engine. Most notably, we use the Scope hierarchy
// directly rather than using TypeLevels.
bool useNewSolver = false;
};
void promoteTypeLevels(TxnLog& log, const TypeArena* arena, TypeLevel minLevel, Scope* outerScope, bool useScope, TypePackId tp);

View file

@ -13,6 +13,10 @@
#include <utility>
LUAU_FASTFLAG(DebugLuauReadWriteProperties)
LUAU_FASTFLAGVARIABLE(LuauDisableCompletionOutsideQuotes, false)
LUAU_FASTFLAGVARIABLE(LuauAnonymousAutofilled, false);
LUAU_FASTFLAGVARIABLE(LuauAutocompleteLastTypecheck, false)
LUAU_FASTFLAGVARIABLE(LuauAutocompleteHideSelfArg, false)
static const std::unordered_set<std::string> kStatementStartingKeywords = {
"while", "if", "local", "repeat", "function", "do", "for", "return", "break", "continue", "type", "export"};
@ -280,18 +284,38 @@ static void autocompleteProps(const Module& module, TypeArena* typeArena, NotNul
ParenthesesRecommendation parens =
indexType == PropIndexType::Key ? ParenthesesRecommendation::None : getParenRecommendation(type, nodes, typeCorrect);
result[name] = AutocompleteEntry{
AutocompleteEntryKind::Property,
type,
prop.deprecated,
isWrongIndexer(type),
typeCorrect,
containingClass,
&prop,
prop.documentationSymbol,
{},
parens,
};
if (FFlag::LuauAutocompleteHideSelfArg)
{
result[name] = AutocompleteEntry{
AutocompleteEntryKind::Property,
type,
prop.deprecated,
isWrongIndexer(type),
typeCorrect,
containingClass,
&prop,
prop.documentationSymbol,
{},
parens,
{},
indexType == PropIndexType::Colon
};
}
else
{
result[name] = AutocompleteEntry{
AutocompleteEntryKind::Property,
type,
prop.deprecated,
isWrongIndexer(type),
typeCorrect,
containingClass,
&prop,
prop.documentationSymbol,
{},
parens
};
}
}
}
};
@ -591,14 +615,14 @@ std::optional<TypeId> getLocalTypeInScopeAt(const Module& module, Position posit
return {};
}
static std::optional<Name> tryGetTypeNameInScope(ScopePtr scope, TypeId ty)
template <typename T>
static std::optional<std::string> tryToStringDetailed(const ScopePtr& scope, T ty, bool functionTypeArguments)
{
if (!canSuggestInferredType(scope, ty))
return std::nullopt;
LUAU_ASSERT(FFlag::LuauAnonymousAutofilled);
ToStringOptions opts;
opts.useLineBreaks = false;
opts.hideTableKind = true;
opts.functionTypeArguments = functionTypeArguments;
opts.scope = scope;
ToStringResult name = toStringDetailed(ty, opts);
@ -608,6 +632,30 @@ static std::optional<Name> tryGetTypeNameInScope(ScopePtr scope, TypeId ty)
return name.name;
}
static std::optional<Name> tryGetTypeNameInScope(ScopePtr scope, TypeId ty, bool functionTypeArguments = false)
{
if (!canSuggestInferredType(scope, ty))
return std::nullopt;
if (FFlag::LuauAnonymousAutofilled)
{
return tryToStringDetailed(scope, ty, functionTypeArguments);
}
else
{
ToStringOptions opts;
opts.useLineBreaks = false;
opts.hideTableKind = true;
opts.scope = scope;
ToStringResult name = toStringDetailed(ty, opts);
if (name.error || name.invalid || name.cycle || name.truncated)
return std::nullopt;
return name.name;
}
}
static bool tryAddTypeCorrectSuggestion(AutocompleteEntryMap& result, ScopePtr scope, AstType* topType, TypeId inferredType, Position position)
{
std::optional<TypeId> ty;
@ -1297,6 +1345,14 @@ static std::optional<AutocompleteEntryMap> autocompleteStringParams(const Source
return std::nullopt;
}
if (FFlag::LuauDisableCompletionOutsideQuotes && !nodes.back()->is<AstExprError>())
{
if (nodes.back()->location.end == position || nodes.back()->location.begin == position)
{
return std::nullopt;
}
}
AstExprCall* candidate = nodes.at(nodes.size() - 2)->as<AstExprCall>();
if (!candidate)
{
@ -1361,6 +1417,140 @@ static AutocompleteResult autocompleteWhileLoopKeywords(std::vector<AstNode*> an
return {std::move(ret), std::move(ancestry), AutocompleteContext::Keyword};
}
static std::string makeAnonymous(const ScopePtr& scope, const FunctionType& funcTy)
{
LUAU_ASSERT(FFlag::LuauAnonymousAutofilled);
std::string result = "function(";
auto [args, tail] = Luau::flatten(funcTy.argTypes);
bool first = true;
// Skip the implicit 'self' argument if call is indexed with ':'
for (size_t argIdx = 0; argIdx < args.size(); ++argIdx)
{
if (!first)
result += ", ";
else
first = false;
std::string name;
if (argIdx < funcTy.argNames.size() && funcTy.argNames[argIdx])
name = funcTy.argNames[argIdx]->name;
else
name = "a" + std::to_string(argIdx);
if (std::optional<Name> type = tryGetTypeNameInScope(scope, args[argIdx], true))
result += name + ": " + *type;
else
result += name;
}
if (tail && (Luau::isVariadic(*tail) || Luau::get<Luau::FreeTypePack>(Luau::follow(*tail))))
{
if (!first)
result += ", ";
std::optional<std::string> varArgType;
if (const VariadicTypePack* pack = get<VariadicTypePack>(follow(*tail)))
{
if (std::optional<std::string> res = tryToStringDetailed(scope, pack->ty, true))
varArgType = std::move(res);
}
if (varArgType)
result += "...: " + *varArgType;
else
result += "...";
}
result += ")";
auto [rets, retTail] = Luau::flatten(funcTy.retTypes);
if (const size_t totalRetSize = rets.size() + (retTail ? 1 : 0); totalRetSize > 0)
{
if (std::optional<std::string> returnTypes = tryToStringDetailed(scope, funcTy.retTypes, true))
{
result += ": ";
bool wrap = totalRetSize != 1;
if (wrap)
result += "(";
result += *returnTypes;
if (wrap)
result += ")";
}
}
result += " end";
return result;
}
static std::optional<AutocompleteEntry> makeAnonymousAutofilled(const ModulePtr& module, Position position, const AstNode* node, const std::vector<AstNode*>& ancestry)
{
LUAU_ASSERT(FFlag::LuauAnonymousAutofilled);
const AstExprCall* call = node->as<AstExprCall>();
if (!call && ancestry.size() > 1)
call = ancestry[ancestry.size() - 2]->as<AstExprCall>();
if (!call)
return std::nullopt;
if (!call->location.containsClosed(position) || call->func->location.containsClosed(position))
return std::nullopt;
TypeId* typeIter = module->astTypes.find(call->func);
if (!typeIter)
return std::nullopt;
const FunctionType* outerFunction = get<FunctionType>(follow(*typeIter));
if (!outerFunction)
return std::nullopt;
size_t argument = 0;
for (size_t i = 0; i < call->args.size; ++i)
{
if (call->args.data[i]->location.containsClosed(position))
{
argument = i;
break;
}
}
if (call->self)
argument++;
std::optional<TypeId> argType;
auto [args, tail] = flatten(outerFunction->argTypes);
if (argument < args.size())
argType = args[argument];
if (!argType)
return std::nullopt;
TypeId followed = follow(*argType);
const FunctionType* type = get<FunctionType>(followed);
if (!type)
{
if (const UnionType* unionType = get<UnionType>(followed))
{
if (std::optional<const FunctionType*> nonnullFunction = returnFirstNonnullOptionOfType<FunctionType>(unionType))
type = *nonnullFunction;
}
}
if (!type)
return std::nullopt;
const ScopePtr scope = findScopeAtPosition(*module, position);
if (!scope)
return std::nullopt;
AutocompleteEntry entry;
entry.kind = AutocompleteEntryKind::GeneratedFunction;
entry.typeCorrect = TypeCorrectKind::Correct;
entry.type = argType;
entry.insertText = makeAnonymous(scope, *type);
return std::make_optional(std::move(entry));
}
static AutocompleteResult autocomplete(const SourceModule& sourceModule, const ModulePtr& module, NotNull<BuiltinTypes> builtinTypes,
TypeArena* typeArena, Scope* globalScope, Position position, StringCompletionCallback callback)
{
@ -1612,7 +1802,19 @@ static AutocompleteResult autocomplete(const SourceModule& sourceModule, const M
return {};
if (node->asExpr())
return autocompleteExpression(sourceModule, *module, builtinTypes, typeArena, ancestry, position);
{
if (FFlag::LuauAnonymousAutofilled)
{
AutocompleteResult ret = autocompleteExpression(sourceModule, *module, builtinTypes, typeArena, ancestry, position);
if (std::optional<AutocompleteEntry> generated = makeAnonymousAutofilled(module, position, node, ancestry))
ret.entryMap[kGeneratedAnonymousFunctionEntryName] = std::move(*generated);
return ret;
}
else
{
return autocompleteExpression(sourceModule, *module, builtinTypes, typeArena, ancestry, position);
}
}
else if (node->asStat())
return {autocompleteStatement(sourceModule, *module, ancestry, position), ancestry, AutocompleteContext::Statement};
@ -1621,11 +1823,14 @@ static AutocompleteResult autocomplete(const SourceModule& sourceModule, const M
AutocompleteResult autocomplete(Frontend& frontend, const ModuleName& moduleName, Position position, StringCompletionCallback callback)
{
// FIXME: We can improve performance here by parsing without checking.
// The old type graph is probably fine. (famous last words!)
FrontendOptions opts;
opts.forAutocomplete = true;
frontend.check(moduleName, opts);
if (!FFlag::LuauAutocompleteLastTypecheck)
{
// FIXME: We can improve performance here by parsing without checking.
// The old type graph is probably fine. (famous last words!)
FrontendOptions opts;
opts.forAutocomplete = true;
frontend.check(moduleName, opts);
}
const SourceModule* sourceModule = frontend.getSourceModule(moduleName);
if (!sourceModule)

View file

@ -4,15 +4,12 @@
#include "Luau/Lexer.h"
#include "Luau/StringUtils.h"
LUAU_FASTFLAGVARIABLE(LuauEnableNonstrictByDefaultForLuauConfig, false)
namespace Luau
{
using Error = std::optional<std::string>;
Config::Config()
: mode(FFlag::LuauEnableNonstrictByDefaultForLuauConfig ? Mode::Nonstrict : Mode::NoCheck)
{
enabledLint.setDefaults();
}

View file

@ -139,7 +139,8 @@ void forEachConstraint(const Checkpoint& start, const Checkpoint& end, const Con
ConstraintGraphBuilder::ConstraintGraphBuilder(ModulePtr module, TypeArena* arena, NotNull<ModuleResolver> moduleResolver,
NotNull<BuiltinTypes> builtinTypes, NotNull<InternalErrorReporter> ice, const ScopePtr& globalScope,
std::function<void(const ModuleName&, const ScopePtr&)> prepareModuleScope, DcrLogger* logger, NotNull<DataFlowGraph> dfg)
std::function<void(const ModuleName&, const ScopePtr&)> prepareModuleScope, DcrLogger* logger, NotNull<DataFlowGraph> dfg,
std::vector<RequireCycle> requireCycles)
: module(module)
, builtinTypes(builtinTypes)
, arena(arena)
@ -149,6 +150,7 @@ ConstraintGraphBuilder::ConstraintGraphBuilder(ModulePtr module, TypeArena* aren
, ice(ice)
, globalScope(globalScope)
, prepareModuleScope(std::move(prepareModuleScope))
, requireCycles(std::move(requireCycles))
, logger(logger)
{
LUAU_ASSERT(module);
@ -703,6 +705,16 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatLocal* l
{
scope->importedTypeBindings[name] = module->exportedTypeBindings;
scope->importedModules[name] = moduleInfo->name;
// Imported types of requires that transitively refer to current module have to be replaced with 'any'
for (const auto& [location, path] : requireCycles)
{
if (!path.empty() && path.front() == moduleInfo->name)
{
for (auto& [name, tf] : scope->importedTypeBindings[name])
tf = TypeFun{{}, {}, builtinTypes->anyType};
}
}
}
}
}
@ -1913,6 +1925,9 @@ std::tuple<TypeId, TypeId, RefinementId> ConstraintGraphBuilder::checkBinary(
NullableBreadcrumbId bc = dfg->getBreadcrumb(typeguard->target);
if (!bc)
return {leftType, rightType, nullptr};
auto augmentForErrorSupression = [&](TypeId ty) -> TypeId {
return arena->addType(UnionType{{ty, builtinTypes->errorType}});
};
TypeId discriminantTy = builtinTypes->neverType;
if (typeguard->type == "nil")
@ -1926,9 +1941,9 @@ std::tuple<TypeId, TypeId, RefinementId> ConstraintGraphBuilder::checkBinary(
else if (typeguard->type == "thread")
discriminantTy = builtinTypes->threadType;
else if (typeguard->type == "table")
discriminantTy = builtinTypes->tableType;
discriminantTy = augmentForErrorSupression(builtinTypes->tableType);
else if (typeguard->type == "function")
discriminantTy = builtinTypes->functionType;
discriminantTy = augmentForErrorSupression(builtinTypes->functionType);
else if (typeguard->type == "userdata")
{
// For now, we don't really care about being accurate with userdata if the typeguard was using typeof.

View file

@ -12,6 +12,7 @@
#include "Luau/ModuleResolver.h"
#include "Luau/Quantify.h"
#include "Luau/Simplify.h"
#include "Luau/TimeTrace.h"
#include "Luau/ToString.h"
#include "Luau/Type.h"
#include "Luau/TypeFamily.h"
@ -259,7 +260,7 @@ struct InstantiationQueuer : TypeOnceVisitor
};
ConstraintSolver::ConstraintSolver(NotNull<Normalizer> normalizer, NotNull<Scope> rootScope, std::vector<NotNull<Constraint>> constraints,
ModuleName moduleName, NotNull<ModuleResolver> moduleResolver, std::vector<RequireCycle> requireCycles, DcrLogger* logger)
ModuleName moduleName, NotNull<ModuleResolver> moduleResolver, std::vector<RequireCycle> requireCycles, DcrLogger* logger, TypeCheckLimits limits)
: arena(normalizer->arena)
, builtinTypes(normalizer->builtinTypes)
, normalizer(normalizer)
@ -269,6 +270,7 @@ ConstraintSolver::ConstraintSolver(NotNull<Normalizer> normalizer, NotNull<Scope
, moduleResolver(moduleResolver)
, requireCycles(requireCycles)
, logger(logger)
, limits(std::move(limits))
{
opts.exhaustive = true;
@ -334,6 +336,11 @@ void ConstraintSolver::run()
continue;
}
if (limits.finishTime && TimeTrace::getClock() > *limits.finishTime)
throwTimeLimitError();
if (limits.cancellationToken && limits.cancellationToken->requested())
throwUserCancelError();
std::string saveMe = FFlag::DebugLuauLogSolver ? toString(*c, opts) : std::string{};
StepSnapshot snapshot;
@ -555,6 +562,9 @@ bool ConstraintSolver::tryDispatch(const InstantiationConstraint& c, NotNull<con
Instantiation inst(TxnLog::empty(), arena, TypeLevel{}, constraint->scope);
if (limits.instantiationChildLimit)
inst.childLimit = *limits.instantiationChildLimit;
std::optional<TypeId> instantiated = inst.substitute(c.superType);
LUAU_ASSERT(get<BlockedType>(c.subType));
@ -586,7 +596,7 @@ bool ConstraintSolver::tryDispatch(const UnaryConstraint& c, NotNull<const Const
if (isBlocked(operandType))
return block(operandType, constraint);
if (get<FreeType>(operandType))
if (!force && get<FreeType>(operandType))
return block(operandType, constraint);
LUAU_ASSERT(get<BlockedType>(c.resultType));
@ -713,6 +723,10 @@ bool ConstraintSolver::tryDispatch(const BinaryConstraint& c, NotNull<const Cons
if (mm)
{
Instantiation instantiation{TxnLog::empty(), arena, TypeLevel{}, constraint->scope};
if (limits.instantiationChildLimit)
instantiation.childLimit = *limits.instantiationChildLimit;
std::optional<TypeId> instantiatedMm = instantiation.substitute(*mm);
if (!instantiatedMm)
{
@ -1318,6 +1332,9 @@ bool ConstraintSolver::tryDispatch(const FunctionCallConstraint& c, NotNull<cons
Instantiation inst(TxnLog::empty(), arena, TypeLevel{}, constraint->scope);
if (limits.instantiationChildLimit)
inst.childLimit = *limits.instantiationChildLimit;
std::vector<TypeId> arityMatchingOverloads;
std::optional<TxnLog> bestOverloadLog;
@ -1334,7 +1351,7 @@ bool ConstraintSolver::tryDispatch(const FunctionCallConstraint& c, NotNull<cons
}
Unifier u{normalizer, constraint->scope, Location{}, Covariant};
u.enableScopeTests();
u.enableNewSolver();
u.tryUnify(*instantiated, inferredTy, /* isFunctionCall */ true);
@ -1384,7 +1401,7 @@ bool ConstraintSolver::tryDispatch(const FunctionCallConstraint& c, NotNull<cons
if (!bestOverloadLog)
{
Unifier u{normalizer, constraint->scope, Location{}, Covariant};
u.enableScopeTests();
u.enableNewSolver();
u.tryUnify(inferredTy, builtinTypes->anyType);
u.tryUnify(fn, builtinTypes->anyType);
@ -1505,6 +1522,7 @@ static void updateTheTableType(
for (size_t i = 0; i < path.size() - 1; ++i)
{
t = follow(t);
auto propTy = findTablePropertyRespectingMeta(builtinTypes, dummy, t, path[i], Location{});
dummy.clear();
@ -1885,19 +1903,20 @@ bool ConstraintSolver::tryDispatch(const RefineConstraint& c, NotNull<const Cons
if (!force && !blockedTypes.empty())
return block(blockedTypes, constraint);
const NormalizedType* normType = normalizer->normalize(c.type);
if (!normType)
reportError(NormalizationTooComplex{}, constraint->location);
if (normType && normType->shouldSuppressErrors())
switch (shouldSuppressErrors(normalizer, c.type))
{
case ErrorSuppression::Suppress:
{
auto resultOrError = simplifyUnion(builtinTypes, arena, result, builtinTypes->errorType).result;
asMutable(c.resultType)->ty.emplace<BoundType>(resultOrError);
break;
}
else
{
case ErrorSuppression::DoNotSuppress:
asMutable(c.resultType)->ty.emplace<BoundType>(result);
break;
case ErrorSuppression::NormalizationFailed:
reportError(NormalizationTooComplex{}, constraint->location);
break;
}
unblock(c.resultType, constraint->location);
@ -1983,6 +2002,15 @@ bool ConstraintSolver::tryDispatchIterableTable(TypeId iteratorTy, const Iterabl
unify(*anyified, ty, constraint->scope);
};
auto unknownify = [&](auto ty) {
Anyification anyify{arena, constraint->scope, builtinTypes, &iceReporter, builtinTypes->unknownType, builtinTypes->anyTypePack};
std::optional anyified = anyify.substitute(ty);
if (!anyified)
reportError(CodeTooComplex{}, constraint->location);
else
unify(*anyified, ty, constraint->scope);
};
auto errorify = [&](auto ty) {
Anyification anyify{arena, constraint->scope, builtinTypes, &iceReporter, errorRecoveryType(), errorRecoveryTypePack()};
std::optional errorified = anyify.substitute(ty);
@ -2051,6 +2079,9 @@ bool ConstraintSolver::tryDispatchIterableTable(TypeId iteratorTy, const Iterabl
Instantiation instantiation(TxnLog::empty(), arena, TypeLevel{}, constraint->scope);
if (limits.instantiationChildLimit)
instantiation.childLimit = *limits.instantiationChildLimit;
if (std::optional<TypeId> instantiatedIterFn = instantiation.substitute(*iterFn))
{
if (auto iterFtv = get<FunctionType>(*instantiatedIterFn))
@ -2107,6 +2138,8 @@ bool ConstraintSolver::tryDispatchIterableTable(TypeId iteratorTy, const Iterabl
LUAU_ASSERT(false);
}
else if (auto primitiveTy = get<PrimitiveType>(iteratorTy); primitiveTy && primitiveTy->type == PrimitiveType::Type::Table)
unknownify(c.variables);
else
errorify(c.variables);
@ -2357,7 +2390,7 @@ template<typename TID>
bool ConstraintSolver::tryUnify(NotNull<const Constraint> constraint, TID subTy, TID superTy)
{
Unifier u{normalizer, constraint->scope, constraint->location, Covariant};
u.enableScopeTests();
u.enableNewSolver();
u.tryUnify(subTy, superTy);
@ -2606,7 +2639,7 @@ bool ConstraintSolver::isBlocked(NotNull<const Constraint> constraint)
ErrorVec ConstraintSolver::unify(TypeId subType, TypeId superType, NotNull<Scope> scope)
{
Unifier u{normalizer, scope, Location{}, Covariant};
u.enableScopeTests();
u.enableNewSolver();
u.tryUnify(subType, superType);
@ -2631,7 +2664,7 @@ ErrorVec ConstraintSolver::unify(TypePackId subPack, TypePackId superPack, NotNu
{
UnifierSharedState sharedState{&iceReporter};
Unifier u{normalizer, scope, Location{}, Covariant};
u.enableScopeTests();
u.enableNewSolver();
u.tryUnify(subPack, superPack);
@ -2728,7 +2761,7 @@ TypeId ConstraintSolver::unionOfTypes(TypeId a, TypeId b, NotNull<Scope> scope,
if (unifyFreeTypes && (get<FreeType>(a) || get<FreeType>(b)))
{
Unifier u{normalizer, scope, Location{}, Covariant};
u.enableScopeTests();
u.enableNewSolver();
u.tryUnify(b, a);
if (u.errors.empty())
@ -2785,4 +2818,14 @@ TypePackId ConstraintSolver::anyifyModuleReturnTypePackGenerics(TypePackId tp)
return arena->addTypePack(resultTypes, resultTail);
}
LUAU_NOINLINE void ConstraintSolver::throwTimeLimitError()
{
throw TimeLimitError(currentModuleName);
}
LUAU_NOINLINE void ConstraintSolver::throwUserCancelError()
{
throw UserCancelError(currentModuleName);
}
} // namespace Luau

View file

@ -1,11 +1,15 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#include "Luau/Differ.h"
#include "Luau/Common.h"
#include "Luau/Error.h"
#include "Luau/ToString.h"
#include "Luau/Type.h"
#include "Luau/TypePack.h"
#include "Luau/Unifiable.h"
#include <optional>
#include <string>
#include <unordered_set>
#include <vector>
namespace Luau
{
@ -34,6 +38,10 @@ std::string DiffPathNode::toString() const
// Add 1 because Lua is 1-indexed
return "Ret[" + std::to_string(*index + 1) + "]";
}
case DiffPathNode::Kind::Negation:
{
return "Negation";
}
default:
{
throw InternalCompilerError{"DiffPathNode::toString is not exhaustive"};
@ -58,22 +66,27 @@ DiffPathNode DiffPathNode::constructWithKind(Kind kind)
DiffPathNodeLeaf DiffPathNodeLeaf::detailsNormal(TypeId ty)
{
return DiffPathNodeLeaf{ty, std::nullopt, std::nullopt, false};
return DiffPathNodeLeaf{ty, std::nullopt, std::nullopt, false, std::nullopt};
}
DiffPathNodeLeaf DiffPathNodeLeaf::detailsTableProperty(TypeId ty, Name tableProperty)
{
return DiffPathNodeLeaf{ty, tableProperty, std::nullopt, false};
return DiffPathNodeLeaf{ty, tableProperty, std::nullopt, false, std::nullopt};
}
DiffPathNodeLeaf DiffPathNodeLeaf::detailsUnionIndex(TypeId ty, size_t index)
{
return DiffPathNodeLeaf{ty, std::nullopt, std::nullopt, false, index};
}
DiffPathNodeLeaf DiffPathNodeLeaf::detailsLength(int minLength, bool isVariadic)
{
return DiffPathNodeLeaf{std::nullopt, std::nullopt, minLength, isVariadic};
return DiffPathNodeLeaf{std::nullopt, std::nullopt, minLength, isVariadic, std::nullopt};
}
DiffPathNodeLeaf DiffPathNodeLeaf::nullopts()
{
return DiffPathNodeLeaf{std::nullopt, std::nullopt, std::nullopt, false};
return DiffPathNodeLeaf{std::nullopt, std::nullopt, std::nullopt, false, std::nullopt};
}
std::string DiffPath::toString(bool prependDot) const
@ -104,7 +117,7 @@ std::string DiffError::toStringALeaf(std::string rootName, const DiffPathNodeLea
checkNonMissingPropertyLeavesHaveNulloptTableProperty();
return pathStr + " has type " + Luau::toString(*leaf.ty);
}
case DiffError::Kind::MissingProperty:
case DiffError::Kind::MissingTableProperty:
{
if (leaf.ty.has_value())
{
@ -120,6 +133,38 @@ std::string DiffError::toStringALeaf(std::string rootName, const DiffPathNodeLea
}
throw InternalCompilerError{"Both leaf.ty and otherLeaf.ty is nullopt"};
}
case DiffError::Kind::MissingUnionMember:
{
// TODO: do normal case
if (leaf.ty.has_value())
{
if (!leaf.unionIndex.has_value())
throw InternalCompilerError{"leaf.unionIndex is nullopt"};
return pathStr + " is a union containing type " + Luau::toString(*leaf.ty);
}
else if (otherLeaf.ty.has_value())
{
return pathStr + " is a union missing type " + Luau::toString(*otherLeaf.ty);
}
throw InternalCompilerError{"Both leaf.ty and otherLeaf.ty is nullopt"};
}
case DiffError::Kind::MissingIntersectionMember:
{
// TODO: better message for intersections
// An intersection of just functions is always an "overloaded function"
// An intersection of just tables is always a "joined table"
if (leaf.ty.has_value())
{
if (!leaf.unionIndex.has_value())
throw InternalCompilerError{"leaf.unionIndex is nullopt"};
return pathStr + " is an intersection containing type " + Luau::toString(*leaf.ty);
}
else if (otherLeaf.ty.has_value())
{
return pathStr + " is an intersection missing type " + Luau::toString(*otherLeaf.ty);
}
throw InternalCompilerError{"Both leaf.ty and otherLeaf.ty is nullopt"};
}
case DiffError::Kind::LengthMismatchInFnArgs:
{
if (!leaf.minLength.has_value())
@ -163,9 +208,20 @@ std::string getDevFixFriendlyName(TypeId ty)
std::string DiffError::toString() const
{
std::string msg = "DiffError: these two types are not equal because the left type at " + toStringALeaf(leftRootName, left, right) +
", while the right type at " + toStringALeaf(rightRootName, right, left);
return msg;
switch (kind)
{
case DiffError::Kind::IncompatibleGeneric:
{
std::string diffPathStr{diffPath.toString(true)};
return "DiffError: these two types are not equal because the left generic at " + leftRootName + diffPathStr +
" cannot be the same type parameter as the right generic at " + rightRootName + diffPathStr;
}
default:
{
return "DiffError: these two types are not equal because the left type at " + toStringALeaf(leftRootName, left, right) +
", while the right type at " + toStringALeaf(rightRootName, right, left);
}
}
}
void DiffError::checkValidInitialization(const DiffPathNodeLeaf& left, const DiffPathNodeLeaf& right)
@ -193,6 +249,19 @@ static DifferResult diffTable(DifferEnvironment& env, TypeId left, TypeId right)
static DifferResult diffPrimitive(DifferEnvironment& env, TypeId left, TypeId right);
static DifferResult diffSingleton(DifferEnvironment& env, TypeId left, TypeId right);
static DifferResult diffFunction(DifferEnvironment& env, TypeId left, TypeId right);
static DifferResult diffGeneric(DifferEnvironment& env, TypeId left, TypeId right);
static DifferResult diffNegation(DifferEnvironment& env, TypeId left, TypeId right);
struct FindSeteqCounterexampleResult
{
// nullopt if no counterexample found
std::optional<size_t> mismatchIdx;
// true if counterexample is in the left, false if cex is in the right
bool inLeft;
};
static FindSeteqCounterexampleResult findSeteqCounterexample(
DifferEnvironment& env, const std::vector<TypeId>& left, const std::vector<TypeId>& right);
static DifferResult diffUnion(DifferEnvironment& env, TypeId left, TypeId right);
static DifferResult diffIntersection(DifferEnvironment& env, TypeId left, TypeId right);
/**
* The last argument gives context info on which complex type contained the TypePack.
*/
@ -205,6 +274,8 @@ static DifferResult diffTable(DifferEnvironment& env, TypeId left, TypeId right)
{
const TableType* leftTable = get<TableType>(left);
const TableType* rightTable = get<TableType>(right);
LUAU_ASSERT(leftTable);
LUAU_ASSERT(rightTable);
for (auto const& [field, value] : leftTable->props)
{
@ -212,7 +283,7 @@ static DifferResult diffTable(DifferEnvironment& env, TypeId left, TypeId right)
{
// left has a field the right doesn't
return DifferResult{DiffError{
DiffError::Kind::MissingProperty,
DiffError::Kind::MissingTableProperty,
DiffPathNodeLeaf::detailsTableProperty(value.type(), field),
DiffPathNodeLeaf::nullopts(),
getDevFixFriendlyName(env.rootLeft),
@ -225,9 +296,9 @@ static DifferResult diffTable(DifferEnvironment& env, TypeId left, TypeId right)
if (leftTable->props.find(field) == leftTable->props.end())
{
// right has a field the left doesn't
return DifferResult{
DiffError{DiffError::Kind::MissingProperty, DiffPathNodeLeaf::nullopts(), DiffPathNodeLeaf::detailsTableProperty(value.type(), field),
getDevFixFriendlyName(env.rootLeft), getDevFixFriendlyName(env.rootRight)}};
return DifferResult{DiffError{DiffError::Kind::MissingTableProperty, DiffPathNodeLeaf::nullopts(),
DiffPathNodeLeaf::detailsTableProperty(value.type(), field), getDevFixFriendlyName(env.rootLeft),
getDevFixFriendlyName(env.rootRight)}};
}
}
// left and right have the same set of keys
@ -248,6 +319,8 @@ static DifferResult diffPrimitive(DifferEnvironment& env, TypeId left, TypeId ri
{
const PrimitiveType* leftPrimitive = get<PrimitiveType>(left);
const PrimitiveType* rightPrimitive = get<PrimitiveType>(right);
LUAU_ASSERT(leftPrimitive);
LUAU_ASSERT(rightPrimitive);
if (leftPrimitive->type != rightPrimitive->type)
{
@ -266,6 +339,8 @@ static DifferResult diffSingleton(DifferEnvironment& env, TypeId left, TypeId ri
{
const SingletonType* leftSingleton = get<SingletonType>(left);
const SingletonType* rightSingleton = get<SingletonType>(right);
LUAU_ASSERT(leftSingleton);
LUAU_ASSERT(rightSingleton);
if (*leftSingleton != *rightSingleton)
{
@ -284,6 +359,8 @@ static DifferResult diffFunction(DifferEnvironment& env, TypeId left, TypeId rig
{
const FunctionType* leftFunction = get<FunctionType>(left);
const FunctionType* rightFunction = get<FunctionType>(right);
LUAU_ASSERT(leftFunction);
LUAU_ASSERT(rightFunction);
DifferResult differResult = diffTpi(env, DiffError::Kind::LengthMismatchInFnArgs, leftFunction->argTypes, rightFunction->argTypes);
if (differResult.diffError.has_value())
@ -291,6 +368,157 @@ static DifferResult diffFunction(DifferEnvironment& env, TypeId left, TypeId rig
return diffTpi(env, DiffError::Kind::LengthMismatchInFnRets, leftFunction->retTypes, rightFunction->retTypes);
}
static DifferResult diffGeneric(DifferEnvironment& env, TypeId left, TypeId right)
{
LUAU_ASSERT(get<GenericType>(left));
LUAU_ASSERT(get<GenericType>(right));
// Try to pair up the generics
bool isLeftFree = !env.genericMatchedPairs.contains(left);
bool isRightFree = !env.genericMatchedPairs.contains(right);
if (isLeftFree && isRightFree)
{
env.genericMatchedPairs[left] = right;
env.genericMatchedPairs[right] = left;
return DifferResult{};
}
else if (isLeftFree || isRightFree)
{
return DifferResult{DiffError{
DiffError::Kind::IncompatibleGeneric,
DiffPathNodeLeaf::nullopts(),
DiffPathNodeLeaf::nullopts(),
getDevFixFriendlyName(env.rootLeft),
getDevFixFriendlyName(env.rootRight),
}};
}
// Both generics are already paired up
if (*env.genericMatchedPairs.find(left) == right)
return DifferResult{};
return DifferResult{DiffError{
DiffError::Kind::IncompatibleGeneric,
DiffPathNodeLeaf::nullopts(),
DiffPathNodeLeaf::nullopts(),
getDevFixFriendlyName(env.rootLeft),
getDevFixFriendlyName(env.rootRight),
}};
}
static DifferResult diffNegation(DifferEnvironment& env, TypeId left, TypeId right)
{
const NegationType* leftNegation = get<NegationType>(left);
const NegationType* rightNegation = get<NegationType>(right);
LUAU_ASSERT(leftNegation);
LUAU_ASSERT(rightNegation);
DifferResult differResult = diffUsingEnv(env, leftNegation->ty, rightNegation->ty);
if (!differResult.diffError.has_value())
return DifferResult{};
differResult.wrapDiffPath(DiffPathNode::constructWithKind(DiffPathNode::Kind::Negation));
return differResult;
}
static FindSeteqCounterexampleResult findSeteqCounterexample(
DifferEnvironment& env, const std::vector<TypeId>& left, const std::vector<TypeId>& right)
{
std::unordered_set<size_t> unmatchedRightIdxes;
for (size_t i = 0; i < right.size(); i++)
unmatchedRightIdxes.insert(i);
for (size_t leftIdx = 0; leftIdx < left.size(); leftIdx++)
{
bool leftIdxIsMatched = false;
auto unmatchedRightIdxIt = unmatchedRightIdxes.begin();
while (unmatchedRightIdxIt != unmatchedRightIdxes.end())
{
DifferResult differResult = diffUsingEnv(env, left[leftIdx], right[*unmatchedRightIdxIt]);
if (differResult.diffError.has_value())
{
unmatchedRightIdxIt++;
continue;
}
// unmatchedRightIdxIt is matched with current leftIdx
leftIdxIsMatched = true;
unmatchedRightIdxIt = unmatchedRightIdxes.erase(unmatchedRightIdxIt);
}
if (!leftIdxIsMatched)
{
return FindSeteqCounterexampleResult{leftIdx, true};
}
}
if (unmatchedRightIdxes.empty())
return FindSeteqCounterexampleResult{std::nullopt, false};
return FindSeteqCounterexampleResult{*unmatchedRightIdxes.begin(), false};
}
static DifferResult diffUnion(DifferEnvironment& env, TypeId left, TypeId right)
{
const UnionType* leftUnion = get<UnionType>(left);
const UnionType* rightUnion = get<UnionType>(right);
LUAU_ASSERT(leftUnion);
LUAU_ASSERT(rightUnion);
FindSeteqCounterexampleResult findSeteqCexResult = findSeteqCounterexample(env, leftUnion->options, rightUnion->options);
if (findSeteqCexResult.mismatchIdx.has_value())
{
if (findSeteqCexResult.inLeft)
return DifferResult{DiffError{
DiffError::Kind::MissingUnionMember,
DiffPathNodeLeaf::detailsUnionIndex(leftUnion->options[*findSeteqCexResult.mismatchIdx], *findSeteqCexResult.mismatchIdx),
DiffPathNodeLeaf::nullopts(),
getDevFixFriendlyName(env.rootLeft),
getDevFixFriendlyName(env.rootRight),
}};
else
return DifferResult{DiffError{
DiffError::Kind::MissingUnionMember,
DiffPathNodeLeaf::nullopts(),
DiffPathNodeLeaf::detailsUnionIndex(rightUnion->options[*findSeteqCexResult.mismatchIdx], *findSeteqCexResult.mismatchIdx),
getDevFixFriendlyName(env.rootLeft),
getDevFixFriendlyName(env.rootRight),
}};
}
// TODO: somehow detect mismatch index, likely using heuristics
return DifferResult{};
}
static DifferResult diffIntersection(DifferEnvironment& env, TypeId left, TypeId right)
{
const IntersectionType* leftIntersection = get<IntersectionType>(left);
const IntersectionType* rightIntersection = get<IntersectionType>(right);
LUAU_ASSERT(leftIntersection);
LUAU_ASSERT(rightIntersection);
FindSeteqCounterexampleResult findSeteqCexResult = findSeteqCounterexample(env, leftIntersection->parts, rightIntersection->parts);
if (findSeteqCexResult.mismatchIdx.has_value())
{
if (findSeteqCexResult.inLeft)
return DifferResult{DiffError{
DiffError::Kind::MissingIntersectionMember,
DiffPathNodeLeaf::detailsUnionIndex(leftIntersection->parts[*findSeteqCexResult.mismatchIdx], *findSeteqCexResult.mismatchIdx),
DiffPathNodeLeaf::nullopts(),
getDevFixFriendlyName(env.rootLeft),
getDevFixFriendlyName(env.rootRight),
}};
else
return DifferResult{DiffError{
DiffError::Kind::MissingIntersectionMember,
DiffPathNodeLeaf::nullopts(),
DiffPathNodeLeaf::detailsUnionIndex(rightIntersection->parts[*findSeteqCexResult.mismatchIdx], *findSeteqCexResult.mismatchIdx),
getDevFixFriendlyName(env.rootLeft),
getDevFixFriendlyName(env.rootRight),
}};
}
// TODO: somehow detect mismatch index, likely using heuristics
return DifferResult{};
}
static DifferResult diffUsingEnv(DifferEnvironment& env, TypeId left, TypeId right)
{
left = follow(left);
@ -322,6 +550,10 @@ static DifferResult diffUsingEnv(DifferEnvironment& env, TypeId left, TypeId rig
// Both left and right must be Any if either is Any for them to be equal!
return DifferResult{};
}
else if (auto ln = get<NegationType>(left))
{
return diffNegation(env, left, right);
}
throw InternalCompilerError{"Unimplemented Simple TypeId variant for diffing"};
}
@ -336,6 +568,24 @@ static DifferResult diffUsingEnv(DifferEnvironment& env, TypeId left, TypeId rig
{
return diffFunction(env, left, right);
}
if (auto lg = get<GenericType>(left))
{
return diffGeneric(env, left, right);
}
if (auto lu = get<UnionType>(left))
{
return diffUnion(env, left, right);
}
if (auto li = get<IntersectionType>(left))
{
return diffIntersection(env, left, right);
}
if (auto le = get<Luau::Unifiable::Error>(left))
{
// TODO: return debug-friendly result state
return DifferResult{};
}
throw InternalCompilerError{"Unimplemented non-simple TypeId variant for diffing"};
}
@ -444,7 +694,7 @@ static DifferResult diffHandleFlattenedTail(DifferEnvironment& env, DiffError::K
DifferResult diff(TypeId ty1, TypeId ty2)
{
DifferEnvironment differEnv{ty1, ty2};
DifferEnvironment differEnv{ty1, ty2, DenseHashMap<TypeId, TypeId>{nullptr}};
return diffUsingEnv(differEnv, ty1, ty2);
}
@ -452,7 +702,7 @@ bool isSimple(TypeId ty)
{
ty = follow(ty);
// TODO: think about GenericType, etc.
return get<PrimitiveType>(ty) || get<SingletonType>(ty) || get<AnyType>(ty);
return get<PrimitiveType>(ty) || get<SingletonType>(ty) || get<AnyType>(ty) || get<NegationType>(ty);
}
} // namespace Luau

View file

@ -1141,22 +1141,26 @@ const SourceModule* Frontend::getSourceModule(const ModuleName& moduleName) cons
ModulePtr check(const SourceModule& sourceModule, const std::vector<RequireCycle>& requireCycles, NotNull<BuiltinTypes> builtinTypes,
NotNull<InternalErrorReporter> iceHandler, NotNull<ModuleResolver> moduleResolver, NotNull<FileResolver> fileResolver,
const ScopePtr& parentScope, std::function<void(const ModuleName&, const ScopePtr&)> prepareModuleScope, FrontendOptions options)
const ScopePtr& parentScope, std::function<void(const ModuleName&, const ScopePtr&)> prepareModuleScope, FrontendOptions options,
TypeCheckLimits limits)
{
const bool recordJsonLog = FFlag::DebugLuauLogSolverToJson;
return check(sourceModule, requireCycles, builtinTypes, iceHandler, moduleResolver, fileResolver, parentScope, std::move(prepareModuleScope),
options, recordJsonLog);
options, limits, recordJsonLog);
}
ModulePtr check(const SourceModule& sourceModule, const std::vector<RequireCycle>& requireCycles, NotNull<BuiltinTypes> builtinTypes,
NotNull<InternalErrorReporter> iceHandler, NotNull<ModuleResolver> moduleResolver, NotNull<FileResolver> fileResolver,
const ScopePtr& parentScope, std::function<void(const ModuleName&, const ScopePtr&)> prepareModuleScope, FrontendOptions options,
bool recordJsonLog)
TypeCheckLimits limits, bool recordJsonLog)
{
ModulePtr result = std::make_shared<Module>();
result->name = sourceModule.name;
result->humanReadableName = sourceModule.humanReadableName;
result->internalTypes.owningModule = result.get();
result->interfaceTypes.owningModule = result.get();
iceHandler->moduleName = sourceModule.name;
std::unique_ptr<DcrLogger> logger;
@ -1174,32 +1178,34 @@ ModulePtr check(const SourceModule& sourceModule, const std::vector<RequireCycle
UnifierSharedState unifierState{iceHandler};
unifierState.counters.recursionLimit = FInt::LuauTypeInferRecursionLimit;
unifierState.counters.iterationLimit = FInt::LuauTypeInferIterationLimit;
unifierState.counters.iterationLimit = limits.unifierIterationLimit.value_or(FInt::LuauTypeInferIterationLimit);
Normalizer normalizer{&result->internalTypes, builtinTypes, NotNull{&unifierState}};
ConstraintGraphBuilder cgb{
result,
&result->internalTypes,
moduleResolver,
builtinTypes,
iceHandler,
parentScope,
std::move(prepareModuleScope),
logger.get(),
NotNull{&dfg},
};
ConstraintGraphBuilder cgb{result, &result->internalTypes, moduleResolver, builtinTypes, iceHandler, parentScope, std::move(prepareModuleScope),
logger.get(), NotNull{&dfg}, requireCycles};
cgb.visit(sourceModule.root);
result->errors = std::move(cgb.errors);
ConstraintSolver cs{
NotNull{&normalizer}, NotNull(cgb.rootScope), borrowConstraints(cgb.constraints), result->name, moduleResolver, requireCycles, logger.get()};
ConstraintSolver cs{NotNull{&normalizer}, NotNull(cgb.rootScope), borrowConstraints(cgb.constraints), result->humanReadableName, moduleResolver,
requireCycles, logger.get(), limits};
if (options.randomizeConstraintResolutionSeed)
cs.randomize(*options.randomizeConstraintResolutionSeed);
cs.run();
try
{
cs.run();
}
catch (const TimeLimitError&)
{
result->timeout = true;
}
catch (const UserCancelError&)
{
result->cancelled = true;
}
for (TypeError& e : cs.errors)
result->errors.emplace_back(std::move(e));
@ -1209,7 +1215,22 @@ ModulePtr check(const SourceModule& sourceModule, const std::vector<RequireCycle
result->clonePublicInterface(builtinTypes, *iceHandler);
Luau::check(builtinTypes, NotNull{&unifierState}, logger.get(), sourceModule, result.get());
if (result->timeout || result->cancelled)
{
// If solver was interrupted, skip typechecking and replace all module results with error-supressing types to avoid leaking blocked/pending types
ScopePtr moduleScope = result->getModuleScope();
moduleScope->returnType = builtinTypes->errorRecoveryTypePack();
for (auto& [name, ty] : result->declaredGlobals)
ty = builtinTypes->errorRecoveryType();
for (auto& [name, tf] : result->exportedTypeBindings)
tf.type = builtinTypes->errorRecoveryType();
}
else
{
Luau::check(builtinTypes, NotNull{&unifierState}, logger.get(), sourceModule, result.get());
}
// It would be nice if we could freeze the arenas before doing type
// checking, but we'll have to do some work to get there.
@ -1248,7 +1269,7 @@ ModulePtr Frontend::check(const SourceModule& sourceModule, Mode mode, std::vect
{
return Luau::check(sourceModule, requireCycles, builtinTypes, NotNull{&iceHandler},
NotNull{forAutocomplete ? &moduleResolverForAutocomplete : &moduleResolver}, NotNull{fileResolver},
environmentScope ? *environmentScope : globals.globalScope, prepareModuleScopeWrap, options, recordJsonLog);
environmentScope ? *environmentScope : globals.globalScope, prepareModuleScopeWrap, options, typeCheckLimits, recordJsonLog);
}
catch (const InternalCompilerError& err)
{

View file

@ -14,6 +14,8 @@
LUAU_FASTINTVARIABLE(LuauSuggestionDistance, 4)
LUAU_FASTFLAGVARIABLE(LuauLintNativeComment, false)
namespace Luau
{
@ -2825,6 +2827,12 @@ static void lintComments(LintContext& context, const std::vector<HotComment>& ho
"optimize directive uses unknown optimization level '%s', 0..2 expected", level);
}
}
else if (FFlag::LuauLintNativeComment && first == "native")
{
if (space != std::string::npos)
emitWarning(context, LintWarning::Code_CommentDirective, hc.location,
"native directive has extra symbols at the end of the line");
}
else
{
static const char* kHotComments[] = {
@ -2833,6 +2841,7 @@ static void lintComments(LintContext& context, const std::vector<HotComment>& ho
"nonstrict",
"strict",
"optimize",
"native",
};
if (const char* suggestion = fuzzyMatch(first, kHotComments, std::size(kHotComments)))

View file

@ -19,7 +19,6 @@ LUAU_FASTINTVARIABLE(LuauNormalizeIterationLimit, 1200);
LUAU_FASTINTVARIABLE(LuauNormalizeCacheLimit, 100000);
LUAU_FASTFLAGVARIABLE(LuauNormalizeBlockedTypes, false);
LUAU_FASTFLAGVARIABLE(LuauNormalizeCyclicUnions, false);
LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution)
LUAU_FASTFLAG(LuauTransitiveSubtyping)
LUAU_FASTFLAG(DebugLuauReadWriteProperties)
@ -253,6 +252,14 @@ bool NormalizedType::shouldSuppressErrors() const
return hasErrors() || get<AnyType>(tops);
}
bool NormalizedType::hasTopTable() const
{
return hasTables() && std::any_of(tables.begin(), tables.end(), [&](TypeId ty) {
auto primTy = get<PrimitiveType>(ty);
return primTy && primTy->type == PrimitiveType::Type::Table;
});
}
bool NormalizedType::hasTops() const
{
return !get<NeverType>(tops);

View file

@ -9,7 +9,6 @@
#include "Luau/VisitType.h"
LUAU_FASTFLAG(DebugLuauSharedSelf)
LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution);
namespace Luau
{

View file

@ -2,6 +2,8 @@
#include "Luau/Scope.h"
LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution);
namespace Luau
{
@ -160,14 +162,12 @@ void Scope::inheritRefinements(const ScopePtr& childScope)
dcrRefinements[k] = a;
}
}
else
for (const auto& [k, a] : childScope->refinements)
{
for (const auto& [k, a] : childScope->refinements)
{
Symbol symbol = getBaseSymbol(k);
if (lookup(symbol))
refinements[k] = a;
}
Symbol symbol = getBaseSymbol(k);
if (lookup(symbol))
refinements[k] = a;
}
}

View file

@ -364,7 +364,13 @@ Relation relate(TypeId left, TypeId right)
if (auto ut = get<UnionType>(left))
return Relation::Intersects;
else if (auto ut = get<UnionType>(right))
{
std::vector<Relation> opts;
for (TypeId part : ut)
if (relate(left, part) == Relation::Subset)
return Relation::Subset;
return Relation::Intersects;
}
if (auto rnt = get<NegationType>(right))
{

View file

@ -3,9 +3,23 @@
#include "Luau/Common.h"
LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution)
namespace Luau
{
bool Symbol::operator==(const Symbol& rhs) const
{
if (local)
return local == rhs.local;
else if (global.value)
return rhs.global.value && global == rhs.global.value; // Subtlety: AstName::operator==(const char*) uses strcmp, not pointer identity.
else if (FFlag::DebugLuauDeferredConstraintResolution)
return !rhs.local && !rhs.global.value; // Reflexivity: we already know `this` Symbol is empty, so check that rhs is.
else
return false;
}
std::string toString(const Symbol& name)
{
if (name.local)

View file

@ -9,8 +9,6 @@
#include <algorithm>
#include <stdexcept>
LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution)
namespace Luau
{

View file

@ -283,7 +283,8 @@ struct TypeChecker2
if (errors.empty())
noTypeFamilyErrors.insert(instance);
reportErrors(std::move(errors));
if (!isErrorSuppressing(location, instance))
reportErrors(std::move(errors));
return instance;
}
@ -488,7 +489,7 @@ struct TypeChecker2
u.hideousFixMeGenericsAreActuallyFree = true;
u.tryUnify(actualRetType, expectedRetType);
const bool ok = u.errors.empty() && u.log.empty();
const bool ok = (u.errors.empty() && u.log.empty()) || isErrorSuppressing(ret->location, actualRetType, ret->location, expectedRetType);
if (!ok)
{
@ -526,9 +527,7 @@ struct TypeChecker2
TypeId valueType = value ? lookupType(value) : nullptr;
if (valueType)
{
ErrorVec errors = tryUnify(stack.back(), value->location, valueType, annotationType);
if (!errors.empty())
reportErrors(std::move(errors));
reportErrors(tryUnify(stack.back(), value->location, valueType, annotationType));
}
visit(var->annotation);
@ -554,9 +553,7 @@ struct TypeChecker2
if (var->annotation)
{
TypeId varType = lookupAnnotation(var->annotation);
ErrorVec errors = tryUnify(stack.back(), value->location, valueTypes.head[j - i], varType);
if (!errors.empty())
reportErrors(std::move(errors));
reportErrors(tryUnify(stack.back(), value->location, valueTypes.head[j - i], varType));
visit(var->annotation);
}
@ -764,6 +761,11 @@ struct TypeChecker2
}
};
const NormalizedType* iteratorNorm = normalizer.normalize(iteratorTy);
if (!iteratorNorm)
reportError(NormalizationTooComplex{}, firstValue->location);
/*
* If the first iterator argument is a function
* * There must be 1 to 3 iterator arguments. Name them (nextTy,
@ -798,7 +800,7 @@ struct TypeChecker2
{
// nothing
}
else if (isOptional(iteratorTy))
else if (isOptional(iteratorTy) && !(iteratorNorm && iteratorNorm->shouldSuppressErrors()))
{
reportError(OptionalValueAccess{iteratorTy}, forInStatement->values.data[0]->location);
}
@ -833,7 +835,7 @@ struct TypeChecker2
{
checkFunction(nextFtv, instantiatedIteratorTypes, true);
}
else
else if (!isErrorSuppressing(forInStatement->values.data[0]->location, *instantiatedNextFn))
{
reportError(CannotCallNonFunction{*instantiatedNextFn}, forInStatement->values.data[0]->location);
}
@ -843,7 +845,7 @@ struct TypeChecker2
reportError(UnificationTooComplex{}, forInStatement->values.data[0]->location);
}
}
else
else if (!isErrorSuppressing(forInStatement->values.data[0]->location, *iterMmTy))
{
// TODO: This will not tell the user that this is because the
// metamethod isn't callable. This is not ideal, and we should
@ -859,7 +861,11 @@ struct TypeChecker2
reportError(UnificationTooComplex{}, forInStatement->values.data[0]->location);
}
}
else
else if (iteratorNorm && iteratorNorm->hasTopTable())
{
// nothing
}
else if (!iteratorNorm || !iteratorNorm->shouldSuppressErrors())
{
reportError(CannotCallNonFunction{iteratorTy}, forInStatement->values.data[0]->location);
}
@ -882,7 +888,9 @@ struct TypeChecker2
if (get<NeverType>(lhsType))
continue;
if (!isSubtype(rhsType, lhsType, stack.back()))
if (!isSubtype(rhsType, lhsType, stack.back()) &&
!isErrorSuppressing(assign->vars.data[i]->location, lhsType, assign->values.data[i]->location, rhsType))
{
reportError(TypeMismatch{lhsType, rhsType}, rhs->location);
}
@ -1064,8 +1072,8 @@ struct TypeChecker2
void visitCall(AstExprCall* call)
{
TypePack args;
std::vector<Location> argLocs;
argLocs.reserve(call->args.size + 1);
std::vector<AstExpr*> argExprs;
argExprs.reserve(call->args.size + 1);
TypeId* originalCallTy = module->astOriginalCallTypes.find(call);
TypeId* selectedOverloadTy = module->astOverloadResolvedTypes.find(call);
@ -1088,18 +1096,18 @@ struct TypeChecker2
ice->ice("method call expression has no 'self'");
args.head.push_back(lookupType(indexExpr->expr));
argLocs.push_back(indexExpr->expr->location);
argExprs.push_back(indexExpr->expr);
}
else if (findMetatableEntry(builtinTypes, module->errors, *originalCallTy, "__call", call->func->location))
{
args.head.insert(args.head.begin(), lookupType(call->func));
argLocs.push_back(call->func->location);
argExprs.push_back(call->func);
}
for (size_t i = 0; i < call->args.size; ++i)
{
AstExpr* arg = call->args.data[i];
argLocs.push_back(arg->location);
argExprs.push_back(arg);
TypeId* argTy = module->astTypes.find(arg);
if (argTy)
args.head.push_back(*argTy);
@ -1127,20 +1135,20 @@ struct TypeChecker2
call->location,
};
resolver.resolve(fnTy, &args, call->func->location, &argLocs);
resolver.resolve(fnTy, &args, call->func, &argExprs);
auto norm = normalizer.normalize(fnTy);
if (!norm)
reportError(NormalizationTooComplex{}, call->func->location);
if (!resolver.ok.empty())
if (norm && norm->shouldSuppressErrors())
return; // error suppressing function type!
else if (!resolver.ok.empty())
return; // We found a call that works, so this is ok.
else if (auto norm = normalizer.normalize(fnTy); !norm || !normalizer.isInhabited(norm))
{
if (!norm)
reportError(NormalizationTooComplex{}, call->func->location);
else
return; // Ok. Calling an uninhabited type is no-op.
}
else if (!norm || !normalizer.isInhabited(norm))
return; // Ok. Calling an uninhabited type is no-op.
else if (!resolver.nonviableOverloads.empty())
{
if (resolver.nonviableOverloads.size() == 1)
if (resolver.nonviableOverloads.size() == 1 && !isErrorSuppressing(call->func->location, resolver.nonviableOverloads.front().first))
reportErrors(resolver.nonviableOverloads.front().second);
else
{
@ -1224,13 +1232,26 @@ struct TypeChecker2
InsertionOrderedMap<TypeId, std::pair<Analysis, size_t>> resolution;
private:
template<typename Ty>
std::optional<ErrorVec> tryUnify(const Location& location, Ty subTy, Ty superTy)
std::optional<ErrorVec> tryUnify(const Location& location, TypeId subTy, TypeId superTy, const LiteralProperties* literalProperties = nullptr)
{
Unifier u{normalizer, scope, location, Covariant};
u.ctx = CountMismatch::Arg;
u.hideousFixMeGenericsAreActuallyFree = true;
u.enableScopeTests();
u.enableNewSolver();
u.tryUnify(subTy, superTy, /*isFunctionCall*/ false, /*isIntersection*/ false, literalProperties);
if (u.errors.empty())
return std::nullopt;
return std::move(u.errors);
}
std::optional<ErrorVec> tryUnify(const Location& location, TypePackId subTy, TypePackId superTy)
{
Unifier u{normalizer, scope, location, Covariant};
u.ctx = CountMismatch::Arg;
u.hideousFixMeGenericsAreActuallyFree = true;
u.enableNewSolver();
u.tryUnify(subTy, superTy);
if (u.errors.empty())
@ -1240,7 +1261,7 @@ struct TypeChecker2
}
std::pair<Analysis, ErrorVec> checkOverload(
TypeId fnTy, const TypePack* args, Location fnLoc, const std::vector<Location>* argLocs, bool callMetamethodOk = true)
TypeId fnTy, const TypePack* args, AstExpr* fnLoc, const std::vector<AstExpr*>* argExprs, bool callMetamethodOk = true)
{
fnTy = follow(fnTy);
@ -1248,25 +1269,64 @@ struct TypeChecker2
if (get<AnyType>(fnTy) || get<ErrorType>(fnTy) || get<NeverType>(fnTy))
return {Ok, {}};
else if (auto fn = get<FunctionType>(fnTy))
return checkOverload_(fnTy, fn, args, fnLoc, argLocs); // Intentionally split to reduce the stack pressure of this function.
return checkOverload_(fnTy, fn, args, fnLoc, argExprs); // Intentionally split to reduce the stack pressure of this function.
else if (auto callMm = findMetatableEntry(builtinTypes, discard, fnTy, "__call", callLoc); callMm && callMetamethodOk)
{
// Calling a metamethod forwards the `fnTy` as self.
TypePack withSelf = *args;
withSelf.head.insert(withSelf.head.begin(), fnTy);
std::vector<Location> withSelfLocs = *argLocs;
withSelfLocs.insert(withSelfLocs.begin(), fnLoc);
std::vector<AstExpr*> withSelfExprs = *argExprs;
withSelfExprs.insert(withSelfExprs.begin(), fnLoc);
return checkOverload(*callMm, &withSelf, fnLoc, &withSelfLocs, /*callMetamethodOk=*/false);
return checkOverload(*callMm, &withSelf, fnLoc, &withSelfExprs, /*callMetamethodOk=*/false);
}
else
return {TypeIsNotAFunction, {}}; // Intentionally empty. We can just fabricate the type error later on.
}
static bool isLiteral(AstExpr* expr)
{
if (auto group = expr->as<AstExprGroup>())
return isLiteral(group->expr);
else if (auto assertion = expr->as<AstExprTypeAssertion>())
return isLiteral(assertion->expr);
return
expr->is<AstExprConstantNil>() ||
expr->is<AstExprConstantBool>() ||
expr->is<AstExprConstantNumber>() ||
expr->is<AstExprConstantString>() ||
expr->is<AstExprFunction>() ||
expr->is<AstExprTable>();
}
static std::unique_ptr<LiteralProperties> buildLiteralPropertiesSet(AstExpr* expr)
{
const AstExprTable* table = expr->as<AstExprTable>();
if (!table)
return nullptr;
std::unique_ptr<LiteralProperties> result = std::make_unique<LiteralProperties>(Name{});
for (const AstExprTable::Item& item : table->items)
{
if (item.kind != AstExprTable::Item::Record)
continue;
AstExprConstantString* keyExpr = item.key->as<AstExprConstantString>();
LUAU_ASSERT(keyExpr);
if (isLiteral(item.value))
result->insert(Name{keyExpr->value.begin(), keyExpr->value.end()});
}
return result;
}
LUAU_NOINLINE
std::pair<Analysis, ErrorVec> checkOverload_(
TypeId fnTy, const FunctionType* fn, const TypePack* args, Location fnLoc, const std::vector<Location>* argLocs)
TypeId fnTy, const FunctionType* fn, const TypePack* args, AstExpr* fnExpr, const std::vector<AstExpr*>* argExprs)
{
TxnLog fake;
FamilyGraphReductionResult result = reduceFamilies(fnTy, callLoc, arena, builtinTypes, scope, normalizer, &fake, /*force=*/true);
@ -1286,9 +1346,11 @@ struct TypeChecker2
TypeId paramTy = *paramIter;
TypeId argTy = args->head[argOffset];
Location argLoc = argLocs->at(argOffset >= argLocs->size() ? argLocs->size() - 1 : argOffset);
AstExpr* argLoc = argExprs->at(argOffset >= argExprs->size() ? argExprs->size() - 1 : argOffset);
if (auto errors = tryUnify(argLoc, argTy, paramTy))
std::unique_ptr<LiteralProperties> literalProperties{buildLiteralPropertiesSet(argLoc)};
if (auto errors = tryUnify(argLoc->location, argTy, paramTy, literalProperties.get()))
{
// Since we're stopping right here, we need to decide if this is a nonviable overload or if there is an arity mismatch.
// If it's a nonviable overload, then we need to keep going to get all type errors.
@ -1308,19 +1370,21 @@ struct TypeChecker2
// If we can iterate over the head of arguments, then we have exhausted the head of the parameters.
LUAU_ASSERT(paramIter == end(fn->argTypes));
Location argLoc = argLocs->at(argOffset >= argLocs->size() ? argLocs->size() - 1 : argOffset);
AstExpr* argExpr = argExprs->at(argOffset >= argExprs->size() ? argExprs->size() - 1 : argOffset);
if (!paramIter.tail())
{
auto [minParams, optMaxParams] = getParameterExtents(TxnLog::empty(), fn->argTypes);
TypeError error{argLoc, CountMismatch{minParams, optMaxParams, args->head.size(), CountMismatch::Arg, false}};
TypeError error{argExpr->location, CountMismatch{minParams, optMaxParams, args->head.size(), CountMismatch::Arg, false}};
return {ArityMismatch, {error}};
}
else if (auto vtp = get<VariadicTypePack>(follow(paramIter.tail())))
{
if (auto errors = tryUnify(argLoc, args->head[argOffset], vtp->ty))
if (auto errors = tryUnify(argExpr->location, args->head[argOffset], vtp->ty))
argumentErrors.insert(argumentErrors.end(), errors->begin(), errors->end());
}
else if (get<GenericTypePack>(follow(paramIter.tail())))
argumentErrors.push_back(TypeError{argExpr->location, TypePackMismatch{fn->argTypes, arena->addTypePack(*args)}});
++argOffset;
}
@ -1333,18 +1397,18 @@ struct TypeChecker2
// It may have a tail, however, so check that.
if (auto vtp = get<VariadicTypePack>(follow(args->tail)))
{
Location argLoc = argLocs->at(argLocs->size() - 1);
AstExpr* argExpr = argExprs->at(argExprs->size() - 1);
if (auto errors = tryUnify(argLoc, vtp->ty, *paramIter))
if (auto errors = tryUnify(argExpr->location, vtp->ty, *paramIter))
argumentErrors.insert(argumentErrors.end(), errors->begin(), errors->end());
}
else if (!isOptional(*paramIter))
{
Location argLoc = argLocs->empty() ? fnLoc : argLocs->at(argLocs->size() - 1);
AstExpr* argExpr = argExprs->empty() ? fnExpr : argExprs->at(argExprs->size() - 1);
// It is ok to have excess parameters as long as they are all optional.
auto [minParams, optMaxParams] = getParameterExtents(TxnLog::empty(), fn->argTypes);
TypeError error{argLoc, CountMismatch{minParams, optMaxParams, args->head.size(), CountMismatch::Arg, false}};
TypeError error{argExpr->location, CountMismatch{minParams, optMaxParams, args->head.size(), CountMismatch::Arg, false}};
return {ArityMismatch, {error}};
}
@ -1355,13 +1419,27 @@ struct TypeChecker2
LUAU_ASSERT(paramIter == end(fn->argTypes));
LUAU_ASSERT(argOffset == args->head.size());
const Location argLoc = argExprs->empty() ? Location{} // TODO
: argExprs->at(argExprs->size() - 1)->location;
if (paramIter.tail() && args->tail)
{
Location argLoc = argLocs->at(argLocs->size() - 1);
if (auto errors = tryUnify(argLoc, *args->tail, *paramIter.tail()))
argumentErrors.insert(argumentErrors.end(), errors->begin(), errors->end());
}
else if (paramIter.tail())
{
const TypePackId paramTail = follow(*paramIter.tail());
if (get<GenericTypePack>(paramTail))
{
argumentErrors.push_back(TypeError{argLoc, TypePackMismatch{fn->argTypes, arena->addTypePack(*args)}});
}
else if (get<VariadicTypePack>(paramTail))
{
// Nothing. This is ok.
}
}
return {argumentErrors.empty() ? Ok : OverloadIsNonviable, argumentErrors};
}
@ -1409,14 +1487,14 @@ struct TypeChecker2
}
public:
void resolve(TypeId fnTy, const TypePack* args, Location selfLoc, const std::vector<Location>* argLocs)
void resolve(TypeId fnTy, const TypePack* args, AstExpr* selfExpr, const std::vector<AstExpr*>* argExprs)
{
fnTy = follow(fnTy);
auto it = get<IntersectionType>(fnTy);
if (!it)
{
auto [analysis, errors] = checkOverload(fnTy, args, selfLoc, argLocs);
auto [analysis, errors] = checkOverload(fnTy, args, selfExpr, argExprs);
add(analysis, fnTy, std::move(errors));
return;
}
@ -1426,7 +1504,7 @@ struct TypeChecker2
if (resolution.find(ty) != resolution.end())
continue;
auto [analysis, errors] = checkOverload(ty, args, selfLoc, argLocs);
auto [analysis, errors] = checkOverload(ty, args, selfExpr, argExprs);
add(analysis, ty, std::move(errors));
}
}
@ -1508,8 +1586,7 @@ struct TypeChecker2
return;
}
// TODO!
visit(indexExpr->expr, ValueContext::LValue);
visit(indexExpr->expr, ValueContext::RValue);
visit(indexExpr->index, ValueContext::RValue);
NotNull<Scope> scope = stack.back();
@ -1576,7 +1653,8 @@ struct TypeChecker2
TypeId inferredArgTy = *argIt;
TypeId annotatedArgTy = lookupAnnotation(arg->annotation);
if (!isSubtype(inferredArgTy, annotatedArgTy, stack.back()))
if (!isSubtype(inferredArgTy, annotatedArgTy, stack.back()) &&
!isErrorSuppressing(arg->location, inferredArgTy, arg->annotation->location, annotatedArgTy))
{
reportError(TypeMismatch{inferredArgTy, annotatedArgTy}, arg->location);
}
@ -1610,7 +1688,7 @@ struct TypeChecker2
TypeId operandType = lookupType(expr->expr);
TypeId resultType = lookupType(expr);
if (get<AnyType>(operandType) || get<ErrorType>(operandType) || get<NeverType>(operandType))
if (isErrorSuppressing(expr->expr->location, operandType))
return;
if (auto it = kUnaryOpMetamethods.find(expr->op); it != kUnaryOpMetamethods.end())
@ -1645,7 +1723,7 @@ struct TypeChecker2
TypeId expectedFunction = testArena.addType(FunctionType{expectedArgs, expectedRet});
ErrorVec errors = tryUnify(scope, expr->location, *mm, expectedFunction);
if (!errors.empty())
if (!errors.empty() && !isErrorSuppressing(expr->expr->location, *firstArg, expr->expr->location, operandType))
{
reportError(TypeMismatch{*firstArg, operandType}, expr->location);
return;
@ -1660,7 +1738,10 @@ struct TypeChecker2
{
DenseHashSet<TypeId> seen{nullptr};
int recursionCount = 0;
const NormalizedType* nty = normalizer.normalize(operandType);
if (nty && nty->shouldSuppressErrors())
return;
if (!hasLength(operandType, seen, &recursionCount))
{
@ -1719,6 +1800,8 @@ struct TypeChecker2
return leftType;
else if (get<AnyType>(rightType) || get<ErrorType>(rightType) || get<NeverType>(rightType))
return rightType;
else if ((normLeft && normLeft->shouldSuppressErrors()) || (normRight && normRight->shouldSuppressErrors()))
return builtinTypes->anyType; // we can't say anything better if it's error suppressing but not any or error alone.
if ((get<BlockedType>(leftType) || get<FreeType>(leftType) || get<GenericType>(leftType)) && !isEquality && !isLogical)
{
@ -1933,6 +2016,9 @@ struct TypeChecker2
case AstExprBinary::Op::CompareLe:
case AstExprBinary::Op::CompareLt:
{
if (normLeft && normLeft->shouldSuppressErrors())
return builtinTypes->numberType;
if (normLeft && normLeft->isExactlyNumber())
{
reportErrors(tryUnify(scope, expr->right->location, rightType, builtinTypes->numberType));
@ -2324,7 +2410,7 @@ struct TypeChecker2
TypeArena arena;
Unifier u{NotNull{&normalizer}, scope, Location{}, Covariant};
u.hideousFixMeGenericsAreActuallyFree = genericsOkay;
u.enableScopeTests();
u.enableNewSolver();
u.tryUnify(subTy, superTy);
const bool ok = u.errors.empty() && u.log.empty();
@ -2338,9 +2424,12 @@ struct TypeChecker2
Unifier u{NotNull{&normalizer}, scope, location, Covariant};
u.ctx = context;
u.hideousFixMeGenericsAreActuallyFree = genericsOkay;
u.enableScopeTests();
u.enableNewSolver();
u.tryUnify(subTy, superTy);
if (isErrorSuppressing(location, subTy, location, superTy))
return {};
return std::move(u.errors);
}
@ -2376,6 +2465,7 @@ struct TypeChecker2
return;
}
// if the type is error suppressing, we don't actually have any work left to do.
if (norm->shouldSuppressErrors())
return;
@ -2542,6 +2632,50 @@ struct TypeChecker2
if (!candidates.empty())
data = TypeErrorData(UnknownPropButFoundLikeProp{utk->table, utk->key, candidates});
}
bool isErrorSuppressing(Location loc, TypeId ty)
{
switch (shouldSuppressErrors(NotNull{&normalizer}, ty))
{
case ErrorSuppression::DoNotSuppress:
return false;
case ErrorSuppression::Suppress:
return true;
case ErrorSuppression::NormalizationFailed:
reportError(NormalizationTooComplex{}, loc);
return false;
};
LUAU_ASSERT(false);
return false; // UNREACHABLE
}
bool isErrorSuppressing(Location loc1, TypeId ty1, Location loc2, TypeId ty2)
{
return isErrorSuppressing(loc1, ty1) || isErrorSuppressing(loc2, ty2);
}
bool isErrorSuppressing(Location loc, TypePackId tp)
{
switch (shouldSuppressErrors(NotNull{&normalizer}, tp))
{
case ErrorSuppression::DoNotSuppress:
return false;
case ErrorSuppression::Suppress:
return true;
case ErrorSuppression::NormalizationFailed:
reportError(NormalizationTooComplex{}, loc);
return false;
};
LUAU_ASSERT(false);
return false; // UNREACHABLE
}
bool isErrorSuppressing(Location loc1, TypePackId tp1, Location loc2, TypePackId tp2)
{
return isErrorSuppressing(loc1, tp1) || isErrorSuppressing(loc2, tp2);
}
};
void check(

View file

@ -36,6 +36,7 @@ LUAU_FASTFLAGVARIABLE(DebugLuauFreezeDuringUnification, false)
LUAU_FASTFLAGVARIABLE(DebugLuauSharedSelf, false)
LUAU_FASTFLAG(LuauInstantiateInSubtyping)
LUAU_FASTFLAGVARIABLE(LuauAllowIndexClassParameters, false)
LUAU_FASTFLAGVARIABLE(LuauFixCyclicModuleExports, false)
LUAU_FASTFLAG(LuauOccursIsntAlwaysFailure)
LUAU_FASTFLAGVARIABLE(LuauTypecheckTypeguards, false)
LUAU_FASTFLAGVARIABLE(LuauTinyControlFlowAnalysis, false)
@ -269,6 +270,8 @@ ModulePtr TypeChecker::checkWithoutRecursionCheck(const SourceModule& module, Mo
currentModule.reset(new Module);
currentModule->name = module.name;
currentModule->humanReadableName = module.humanReadableName;
currentModule->internalTypes.owningModule = currentModule.get();
currentModule->interfaceTypes.owningModule = currentModule.get();
currentModule->type = module.type;
currentModule->allocator = module.allocator;
currentModule->names = module.names;
@ -1193,6 +1196,19 @@ ControlFlow TypeChecker::check(const ScopePtr& scope, const AstStatLocal& local)
{
scope->importedTypeBindings[name] = module->exportedTypeBindings;
scope->importedModules[name] = moduleInfo->name;
if (FFlag::LuauFixCyclicModuleExports)
{
// Imported types of requires that transitively refer to current module have to be replaced with 'any'
for (const auto& [location, path] : requireCycles)
{
if (!path.empty() && path.front() == moduleInfo->name)
{
for (auto& [name, tf] : scope->importedTypeBindings[name])
tf = TypeFun{{}, {}, anyType};
}
}
}
}
// In non-strict mode we force the module type on the variable, in strict mode it is already unified

View file

@ -295,4 +295,58 @@ TypeId stripNil(NotNull<BuiltinTypes> builtinTypes, TypeArena& arena, TypeId ty)
return follow(ty);
}
ErrorSuppression shouldSuppressErrors(NotNull<Normalizer> normalizer, TypeId ty)
{
const NormalizedType* normType = normalizer->normalize(ty);
if (!normType)
return ErrorSuppression::NormalizationFailed;
return (normType->shouldSuppressErrors()) ? ErrorSuppression::Suppress : ErrorSuppression::DoNotSuppress;
}
ErrorSuppression shouldSuppressErrors(NotNull<Normalizer> normalizer, TypePackId tp)
{
auto [tys, tail] = flatten(tp);
// check the head, one type at a time
for (TypeId ty : tys)
{
auto result = shouldSuppressErrors(normalizer, ty);
if (result != ErrorSuppression::DoNotSuppress)
return result;
}
// check the tail if we have one and it's finite
if (tail && finite(*tail))
return shouldSuppressErrors(normalizer, *tail);
return ErrorSuppression::DoNotSuppress;
}
// This is a useful helper because it is often the case that we are looking at specifically a pair of types that might suppress.
ErrorSuppression shouldSuppressErrors(NotNull<Normalizer> normalizer, TypeId ty1, TypeId ty2)
{
auto result = shouldSuppressErrors(normalizer, ty1);
// if ty1 is do not suppress, ty2 determines our overall behavior
if (result == ErrorSuppression::DoNotSuppress)
return shouldSuppressErrors(normalizer, ty2);
// otherwise, ty1 is either suppress or normalization failure which are both the appropriate overarching result
return result;
}
ErrorSuppression shouldSuppressErrors(NotNull<Normalizer> normalizer, TypePackId tp1, TypePackId tp2)
{
auto result = shouldSuppressErrors(normalizer, tp1);
// if tp1 is do not suppress, tp2 determines our overall behavior
if (result == ErrorSuppression::DoNotSuppress)
return shouldSuppressErrors(normalizer, tp2);
// otherwise, tp1 is either suppress or normalization failure which are both the appropriate overarching result
return result;
}
} // namespace Luau

View file

@ -22,7 +22,6 @@ LUAU_FASTFLAGVARIABLE(LuauInstantiateInSubtyping, false)
LUAU_FASTFLAGVARIABLE(LuauMaintainScopesInUnifier, false)
LUAU_FASTFLAGVARIABLE(LuauTransitiveSubtyping, false)
LUAU_FASTFLAGVARIABLE(LuauOccursIsntAlwaysFailure, false)
LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution)
LUAU_FASTFLAG(LuauNormalizeBlockedTypes)
LUAU_FASTFLAG(LuauAlwaysCommitInferencesOfFunctionCalls)
@ -50,7 +49,7 @@ struct PromoteTypeLevels final : TypeOnceVisitor
template<typename TID, typename T>
void promote(TID ty, T* t)
{
if (FFlag::DebugLuauDeferredConstraintResolution && !t)
if (useScopes && !t)
return;
LUAU_ASSERT(t);
@ -369,7 +368,6 @@ static std::optional<std::pair<Luau::Name, const SingletonType*>> getTableMatchT
return std::nullopt;
}
// TODO: Inline and clip with FFlag::DebugLuauDeferredConstraintResolution
template<typename TY_A, typename TY_B>
static bool subsumes(bool useScopes, TY_A* left, TY_B* right)
{
@ -406,11 +404,11 @@ Unifier::Unifier(NotNull<Normalizer> normalizer, NotNull<Scope> scope, const Loc
LUAU_ASSERT(sharedState.iceHandler);
}
void Unifier::tryUnify(TypeId subTy, TypeId superTy, bool isFunctionCall, bool isIntersection)
void Unifier::tryUnify(TypeId subTy, TypeId superTy, bool isFunctionCall, bool isIntersection, const LiteralProperties* literalProperties)
{
sharedState.counters.iterationCount = 0;
tryUnify_(subTy, superTy, isFunctionCall, isIntersection);
tryUnify_(subTy, superTy, isFunctionCall, isIntersection, literalProperties);
}
static bool isBlocked(const TxnLog& log, TypeId ty)
@ -425,7 +423,7 @@ static bool isBlocked(const TxnLog& log, TypePackId tp)
return get<BlockedTypePack>(tp);
}
void Unifier::tryUnify_(TypeId subTy, TypeId superTy, bool isFunctionCall, bool isIntersection)
void Unifier::tryUnify_(TypeId subTy, TypeId superTy, bool isFunctionCall, bool isIntersection, const LiteralProperties* literalProperties)
{
RecursionLimiter _ra(&sharedState.counters.recursionCount, sharedState.counters.recursionLimit);
@ -443,6 +441,16 @@ void Unifier::tryUnify_(TypeId subTy, TypeId superTy, bool isFunctionCall, bool
if (superTy == subTy)
return;
if (isBlocked(log, subTy) && isBlocked(log, superTy))
{
blockedTypes.push_back(subTy);
blockedTypes.push_back(superTy);
}
else if (isBlocked(log, subTy))
blockedTypes.push_back(subTy);
else if (isBlocked(log, superTy))
blockedTypes.push_back(superTy);
if (log.get<TypeFamilyInstanceType>(superTy))
{
// We do not report errors from reducing here. This is because we will
@ -470,7 +478,7 @@ void Unifier::tryUnify_(TypeId subTy, TypeId superTy, bool isFunctionCall, bool
auto superFree = log.getMutable<FreeType>(superTy);
auto subFree = log.getMutable<FreeType>(subTy);
if (superFree && subFree && subsumes(useScopes, superFree, subFree))
if (superFree && subFree && subsumes(useNewSolver, superFree, subFree))
{
if (!occursCheck(subTy, superTy, /* reversed = */ false))
log.replace(subTy, BoundType(superTy));
@ -481,7 +489,7 @@ void Unifier::tryUnify_(TypeId subTy, TypeId superTy, bool isFunctionCall, bool
{
if (!occursCheck(superTy, subTy, /* reversed = */ true))
{
if (subsumes(useScopes, superFree, subFree))
if (subsumes(useNewSolver, superFree, subFree))
{
log.changeLevel(subTy, superFree->level);
}
@ -495,7 +503,7 @@ void Unifier::tryUnify_(TypeId subTy, TypeId superTy, bool isFunctionCall, bool
{
// Unification can't change the level of a generic.
auto subGeneric = log.getMutable<GenericType>(subTy);
if (subGeneric && !subsumes(useScopes, subGeneric, superFree))
if (subGeneric && !subsumes(useNewSolver, subGeneric, superFree))
{
// TODO: a more informative error message? CLI-39912
reportError(location, GenericError{"Generic subtype escaping scope"});
@ -504,7 +512,7 @@ void Unifier::tryUnify_(TypeId subTy, TypeId superTy, bool isFunctionCall, bool
if (!occursCheck(superTy, subTy, /* reversed = */ true))
{
promoteTypeLevels(log, types, superFree->level, superFree->scope, useScopes, subTy);
promoteTypeLevels(log, types, superFree->level, superFree->scope, useNewSolver, subTy);
Widen widen{types, builtinTypes};
log.replace(superTy, BoundType(widen(subTy)));
@ -521,7 +529,7 @@ void Unifier::tryUnify_(TypeId subTy, TypeId superTy, bool isFunctionCall, bool
// Unification can't change the level of a generic.
auto superGeneric = log.getMutable<GenericType>(superTy);
if (superGeneric && !subsumes(useScopes, superGeneric, subFree))
if (superGeneric && !subsumes(useNewSolver, superGeneric, subFree))
{
// TODO: a more informative error message? CLI-39912
reportError(location, GenericError{"Generic supertype escaping scope"});
@ -530,7 +538,7 @@ void Unifier::tryUnify_(TypeId subTy, TypeId superTy, bool isFunctionCall, bool
if (!occursCheck(subTy, superTy, /* reversed = */ false))
{
promoteTypeLevels(log, types, subFree->level, subFree->scope, useScopes, superTy);
promoteTypeLevels(log, types, subFree->level, subFree->scope, useNewSolver, superTy);
log.replace(subTy, BoundType(superTy));
}
@ -542,7 +550,7 @@ void Unifier::tryUnify_(TypeId subTy, TypeId superTy, bool isFunctionCall, bool
auto superGeneric = log.getMutable<GenericType>(superTy);
auto subGeneric = log.getMutable<GenericType>(subTy);
if (superGeneric && subGeneric && subsumes(useScopes, superGeneric, subGeneric))
if (superGeneric && subGeneric && subsumes(useNewSolver, superGeneric, subGeneric))
{
if (!occursCheck(subTy, superTy, /* reversed = */ false))
log.replace(subTy, BoundType(superTy));
@ -637,16 +645,7 @@ void Unifier::tryUnify_(TypeId subTy, TypeId superTy, bool isFunctionCall, bool
size_t errorCount = errors.size();
if (isBlocked(log, subTy) && isBlocked(log, superTy))
{
blockedTypes.push_back(subTy);
blockedTypes.push_back(superTy);
}
else if (isBlocked(log, subTy))
blockedTypes.push_back(subTy);
else if (isBlocked(log, superTy))
blockedTypes.push_back(superTy);
else if (const UnionType* subUnion = log.getMutable<UnionType>(subTy))
if (const UnionType* subUnion = log.getMutable<UnionType>(subTy))
{
tryUnifyUnionWithType(subTy, subUnion, superTy);
}
@ -717,7 +716,7 @@ void Unifier::tryUnify_(TypeId subTy, TypeId superTy, bool isFunctionCall, bool
else if (log.getMutable<TableType>(superTy) && log.getMutable<TableType>(subTy))
{
tryUnifyTables(subTy, superTy, isIntersection);
tryUnifyTables(subTy, superTy, isIntersection, literalProperties);
}
else if (log.get<TableType>(superTy) && (log.get<PrimitiveType>(subTy) || log.get<SingletonType>(subTy)))
{
@ -772,7 +771,7 @@ void Unifier::tryUnifyUnionWithType(TypeId subTy, const UnionType* subUnion, Typ
Unifier innerState = makeChildUnifier();
innerState.tryUnify_(type, superTy);
if (FFlag::DebugLuauDeferredConstraintResolution)
if (useNewSolver)
logs.push_back(std::move(innerState.log));
if (auto e = hasUnificationTooComplex(innerState.errors))
@ -955,7 +954,7 @@ void Unifier::tryUnifyTypeWithUnion(TypeId subTy, TypeId superTy, const UnionTyp
if (FFlag::LuauTransitiveSubtyping ? !innerState.failure : innerState.errors.empty())
{
found = true;
if (FFlag::DebugLuauDeferredConstraintResolution)
if (useNewSolver)
logs.push_back(std::move(innerState.log));
else
{
@ -980,7 +979,7 @@ void Unifier::tryUnifyTypeWithUnion(TypeId subTy, TypeId superTy, const UnionTyp
}
}
if (FFlag::DebugLuauDeferredConstraintResolution)
if (useNewSolver)
log.concatAsUnion(combineLogsIntoUnion(std::move(logs)), NotNull{types});
if (unificationTooComplex)
@ -1061,14 +1060,14 @@ void Unifier::tryUnifyTypeWithIntersection(TypeId subTy, TypeId superTy, const I
firstFailedOption = {innerState.errors.front()};
}
if (FFlag::DebugLuauDeferredConstraintResolution)
if (useNewSolver)
logs.push_back(std::move(innerState.log));
else
log.concat(std::move(innerState.log));
failure |= innerState.failure;
}
if (FFlag::DebugLuauDeferredConstraintResolution)
if (useNewSolver)
log.concat(combineLogsIntoIntersection(std::move(logs)));
if (unificationTooComplex)
@ -1118,7 +1117,7 @@ void Unifier::tryUnifyIntersectionWithType(TypeId subTy, const IntersectionType*
}
}
if (FFlag::DebugLuauDeferredConstraintResolution && normalize)
if (useNewSolver && normalize)
{
// We cannot normalize a type that contains blocked types. We have to
// stop for now if we find any.
@ -1161,7 +1160,7 @@ void Unifier::tryUnifyIntersectionWithType(TypeId subTy, const IntersectionType*
{
found = true;
errorsSuppressed = innerState.failure;
if (FFlag::DebugLuauDeferredConstraintResolution || (FFlag::LuauTransitiveSubtyping && innerState.failure))
if (useNewSolver || (FFlag::LuauTransitiveSubtyping && innerState.failure))
logs.push_back(std::move(innerState.log));
else
{
@ -1176,7 +1175,7 @@ void Unifier::tryUnifyIntersectionWithType(TypeId subTy, const IntersectionType*
}
}
if (FFlag::DebugLuauDeferredConstraintResolution)
if (useNewSolver)
log.concat(combineLogsIntoIntersection(std::move(logs)));
else if (FFlag::LuauTransitiveSubtyping && errorsSuppressed)
log.concat(std::move(logs.front()));
@ -1296,7 +1295,7 @@ void Unifier::tryUnifyNormalizedTypes(
}
}
if (FFlag::DebugLuauDeferredConstraintResolution)
if (useNewSolver)
{
for (TypeId superTable : superNorm.tables)
{
@ -1527,6 +1526,15 @@ struct WeirdIter
return pack != nullptr && index < pack->head.size();
}
std::optional<TypePackId> tail() const
{
if (!pack)
return packId;
LUAU_ASSERT(index == pack->head.size());
return pack->tail;
}
bool advance()
{
if (!pack)
@ -1588,9 +1596,9 @@ struct WeirdIter
}
};
void Unifier::enableScopeTests()
void Unifier::enableNewSolver()
{
useScopes = true;
useNewSolver = true;
log.useScopes = true;
}
@ -1664,25 +1672,26 @@ void Unifier::tryUnify_(TypePackId subTp, TypePackId superTp, bool isFunctionCal
blockedTypePacks.push_back(superTp);
}
else if (isBlocked(log, subTp))
{
blockedTypePacks.push_back(subTp);
}
else if (isBlocked(log, superTp))
{
blockedTypePacks.push_back(superTp);
}
if (log.getMutable<FreeTypePack>(superTp))
if (auto superFree = log.getMutable<FreeTypePack>(superTp))
{
if (!occursCheck(superTp, subTp, /* reversed = */ true))
{
Widen widen{types, builtinTypes};
if (useNewSolver)
promoteTypeLevels(log, types, superFree->level, superFree->scope, /*useScopes*/ true, subTp);
log.replace(superTp, Unifiable::Bound<TypePackId>(widen(subTp)));
}
}
else if (log.getMutable<FreeTypePack>(subTp))
else if (auto subFree = log.getMutable<FreeTypePack>(subTp))
{
if (!occursCheck(subTp, superTp, /* reversed = */ false))
{
if (useNewSolver)
promoteTypeLevels(log, types, subFree->level, subFree->scope, /*useScopes*/ true, superTp);
log.replace(subTp, Unifiable::Bound<TypePackId>(superTp));
}
}
@ -1771,28 +1780,74 @@ void Unifier::tryUnify_(TypePackId subTp, TypePackId superTp, bool isFunctionCal
// If both are at the end, we're done
if (!superIter.good() && !subIter.good())
{
const bool lFreeTail = superTpv->tail && log.getMutable<FreeTypePack>(log.follow(*superTpv->tail)) != nullptr;
const bool rFreeTail = subTpv->tail && log.getMutable<FreeTypePack>(log.follow(*subTpv->tail)) != nullptr;
if (lFreeTail && rFreeTail)
if (useNewSolver)
{
tryUnify_(*subTpv->tail, *superTpv->tail);
}
else if (lFreeTail)
{
tryUnify_(emptyTp, *superTpv->tail);
}
else if (rFreeTail)
{
tryUnify_(emptyTp, *subTpv->tail);
}
else if (subTpv->tail && superTpv->tail)
{
if (log.getMutable<VariadicTypePack>(superIter.packId))
tryUnifyVariadics(subIter.packId, superIter.packId, false, int(subIter.index));
else if (log.getMutable<VariadicTypePack>(subIter.packId))
tryUnifyVariadics(superIter.packId, subIter.packId, true, int(superIter.index));
if (subIter.tail() && superIter.tail())
tryUnify_(*subIter.tail(), *superIter.tail());
else if (subIter.tail())
{
const TypePackId subTail = log.follow(*subIter.tail());
if (log.get<FreeTypePack>(subTail))
tryUnify_(subTail, emptyTp);
else if (log.get<GenericTypePack>(subTail))
reportError(location, TypePackMismatch{subTail, emptyTp});
else if (log.get<VariadicTypePack>(subTail) || log.get<ErrorTypePack>(subTail))
{
// Nothing. This is ok.
}
else
{
ice("Unexpected subtype tail pack " + toString(subTail), location);
}
}
else if (superIter.tail())
{
const TypePackId superTail = log.follow(*superIter.tail());
if (log.get<FreeTypePack>(superTail))
tryUnify_(emptyTp, superTail);
else if (log.get<GenericTypePack>(superTail))
reportError(location, TypePackMismatch{emptyTp, superTail});
else if (log.get<VariadicTypePack>(superTail) || log.get<ErrorTypePack>(superTail))
{
// Nothing. This is ok.
}
else
{
ice("Unexpected supertype tail pack " + toString(superTail), location);
}
}
else
{
// Nothing. This is ok.
}
}
else
{
const bool lFreeTail = superTpv->tail && log.getMutable<FreeTypePack>(log.follow(*superTpv->tail)) != nullptr;
const bool rFreeTail = subTpv->tail && log.getMutable<FreeTypePack>(log.follow(*subTpv->tail)) != nullptr;
if (lFreeTail && rFreeTail)
{
tryUnify_(*subTpv->tail, *superTpv->tail);
}
else if (lFreeTail)
{
tryUnify_(emptyTp, *superTpv->tail);
}
else if (rFreeTail)
{
tryUnify_(emptyTp, *subTpv->tail);
}
else if (subTpv->tail && superTpv->tail)
{
if (log.getMutable<VariadicTypePack>(superIter.packId))
tryUnifyVariadics(subIter.packId, superIter.packId, false, int(subIter.index));
else if (log.getMutable<VariadicTypePack>(subIter.packId))
tryUnifyVariadics(superIter.packId, subIter.packId, true, int(superIter.index));
else
tryUnify_(*subTpv->tail, *superTpv->tail);
}
}
break;
@ -2049,7 +2104,7 @@ struct Resetter
} // namespace
void Unifier::tryUnifyTables(TypeId subTy, TypeId superTy, bool isIntersection)
void Unifier::tryUnifyTables(TypeId subTy, TypeId superTy, bool isIntersection, const LiteralProperties* literalProperties)
{
if (isPrim(log.follow(subTy), PrimitiveType::Table))
subTy = builtinTypes->emptyTableType;
@ -2134,7 +2189,8 @@ void Unifier::tryUnifyTables(TypeId subTy, TypeId superTy, bool isIntersection)
{
// TODO: read-only properties don't need invariance
Resetter resetter{&variance};
variance = Invariant;
if (!literalProperties || !literalProperties->contains(name))
variance = Invariant;
Unifier innerState = makeChildUnifier();
innerState.tryUnify_(r->second.type(), prop.type());
@ -2150,7 +2206,8 @@ void Unifier::tryUnifyTables(TypeId subTy, TypeId superTy, bool isIntersection)
// TODO: read-only indexers don't need invariance
// TODO: really we should only allow this if prop.type is optional.
Resetter resetter{&variance};
variance = Invariant;
if (!literalProperties || !literalProperties->contains(name))
variance = Invariant;
Unifier innerState = makeChildUnifier();
innerState.tryUnify_(subTable->indexer->indexResultType, prop.type());
@ -2213,10 +2270,17 @@ void Unifier::tryUnifyTables(TypeId subTy, TypeId superTy, bool isIntersection)
// TODO: read-only indexers don't need invariance
// TODO: really we should only allow this if prop.type is optional.
Resetter resetter{&variance};
variance = Invariant;
if (!literalProperties || !literalProperties->contains(name))
variance = Invariant;
Unifier innerState = makeChildUnifier();
innerState.tryUnify_(superTable->indexer->indexResultType, prop.type());
if (useNewSolver)
innerState.tryUnify_(prop.type(), superTable->indexer->indexResultType);
else
{
// Incredibly, the old solver depends on this bug somehow.
innerState.tryUnify_(superTable->indexer->indexResultType, prop.type());
}
checkChildUnifierTypeMismatch(innerState.errors, name, superTy, subTy);
@ -2478,7 +2542,7 @@ void Unifier::tryUnifyWithMetatable(TypeId subTy, TypeId superTy, bool reversed)
{
case TableState::Free:
{
if (FFlag::DebugLuauDeferredConstraintResolution)
if (useNewSolver)
{
Unifier innerState = makeChildUnifier();
bool missingProperty = false;
@ -2843,8 +2907,8 @@ std::optional<TypeId> Unifier::findTablePropertyRespectingMeta(TypeId lhsType, N
TxnLog Unifier::combineLogsIntoIntersection(std::vector<TxnLog> logs)
{
LUAU_ASSERT(FFlag::DebugLuauDeferredConstraintResolution);
TxnLog result(useScopes);
LUAU_ASSERT(useNewSolver);
TxnLog result(useNewSolver);
for (TxnLog& log : logs)
result.concatAsIntersections(std::move(log), NotNull{types});
return result;
@ -2852,7 +2916,7 @@ TxnLog Unifier::combineLogsIntoIntersection(std::vector<TxnLog> logs)
TxnLog Unifier::combineLogsIntoUnion(std::vector<TxnLog> logs)
{
TxnLog result(useScopes);
TxnLog result(useNewSolver);
for (TxnLog& log : logs)
result.concatAsUnion(std::move(log), NotNull{types});
return result;
@ -3012,8 +3076,8 @@ Unifier Unifier::makeChildUnifier()
u.normalize = normalize;
u.checkInhabited = checkInhabited;
if (useScopes)
u.enableScopeTests();
if (useNewSolver)
u.enableNewSolver();
return u;
}

View file

@ -274,9 +274,6 @@ std::string runCode(lua_State* L, const std::string& source)
return error;
}
if (codegen)
Luau::CodeGen::compile(L, -1);
lua_State* T = lua_newthread(L);
lua_pushvalue(L, -2);

View file

@ -12,12 +12,18 @@ namespace Luau
namespace CodeGen
{
enum CodeGenFlags
{
// Only run native codegen for modules that have been marked with --!native
CodeGen_OnlyNativeModules = 1 << 0,
};
bool isSupported();
void create(lua_State* L);
// Builds target function and all inner functions
void compile(lua_State* L, int idx);
void compile(lua_State* L, int idx, unsigned int flags = 0);
using AnnotatorFn = void (*)(void* context, std::string& result, int fid, int instpos);

View file

@ -77,6 +77,12 @@ enum class IrCmd : uint8_t
// B: unsigned int (hash)
GET_HASH_NODE_ADDR,
// Get pointer (TValue) to Closure upvalue.
// A: pointer or undef (Closure)
// B: UPn
// When undef is specified, uses current function Closure.
GET_CLOSURE_UPVAL_ADDR,
// Store a tag into TValue
// A: Rn
// B: tag
@ -542,10 +548,10 @@ enum class IrCmd : uint8_t
FALLBACK_GETVARARGS,
// Create closure from a child proto
// A: unsigned int (bytecode instruction index)
// B: Rn (dest)
// A: unsigned int (nups)
// B: pointer (table)
// C: unsigned int (protoid)
FALLBACK_NEWCLOSURE,
NEWCLOSURE,
// Create closure from a pre-created function object (reusing it unless environments diverge)
// A: unsigned int (bytecode instruction index)
@ -600,6 +606,10 @@ enum class IrCmd : uint8_t
// Returns the string name of a type either from a __type metatable field or just based on the tag, alternative for typeof(x)
// A: Rn
GET_TYPEOF,
// Find or create an upval at the given level
// A: Rn (level)
FINDUPVAL,
};
enum class IrConstKind : uint8_t

View file

@ -150,6 +150,7 @@ inline bool hasResult(IrCmd cmd)
case IrCmd::GET_ARR_ADDR:
case IrCmd::GET_SLOT_NODE_ADDR:
case IrCmd::GET_HASH_NODE_ADDR:
case IrCmd::GET_CLOSURE_UPVAL_ADDR:
case IrCmd::ADD_INT:
case IrCmd::SUB_INT:
case IrCmd::ADD_NUM:
@ -192,6 +193,8 @@ inline bool hasResult(IrCmd cmd)
case IrCmd::INVOKE_LIBM:
case IrCmd::GET_TYPE:
case IrCmd::GET_TYPEOF:
case IrCmd::NEWCLOSURE:
case IrCmd::FINDUPVAL:
return true;
default:
break;

View file

@ -6,7 +6,7 @@
#define LUACODEGEN_API extern
#endif
struct lua_State;
typedef struct lua_State lua_State;
// returns 1 if Luau code generator is supported, 0 otherwise
LUACODEGEN_API int luau_codegen_supported(void);

View file

@ -239,7 +239,7 @@ void create(lua_State* L)
ecb->enter = onEnter;
}
void compile(lua_State* L, int idx)
void compile(lua_State* L, int idx, unsigned int flags)
{
LUAU_ASSERT(lua_isLfunction(L, idx));
const TValue* func = luaA_toobject(L, idx);
@ -249,6 +249,13 @@ void compile(lua_State* L, int idx)
if (!data)
return;
Proto* root = clvalue(func)->l.p;
if ((flags & CodeGen_OnlyNativeModules) != 0 && (root->flags & LPF_NATIVE_MODULE) == 0)
return;
std::vector<Proto*> protos;
gatherFunctions(protos, root);
#if defined(__aarch64__)
static unsigned int cpuFeatures = getCpuFeaturesA64();
A64::AssemblyBuilderA64 build(/* logText= */ false, cpuFeatures);
@ -256,9 +263,6 @@ void compile(lua_State* L, int idx)
X64::AssemblyBuilderX64 build(/* logText= */ false);
#endif
std::vector<Proto*> protos;
gatherFunctions(protos, clvalue(func)->l.p);
ModuleHelpers helpers;
#if defined(__aarch64__)
A64::assembleHelpers(build, helpers);

View file

@ -427,9 +427,6 @@ static RegisterSet computeBlockLiveInRegSet(IrFunction& function, const IrBlock&
case IrCmd::FALLBACK_GETVARARGS:
defRange(vmRegOp(inst.b), function.intOp(inst.c));
break;
case IrCmd::FALLBACK_NEWCLOSURE:
def(inst.b);
break;
case IrCmd::FALLBACK_DUPCLOSURE:
def(inst.b);
break;
@ -448,6 +445,10 @@ static RegisterSet computeBlockLiveInRegSet(IrFunction& function, const IrBlock&
use(inst.a);
break;
case IrCmd::FINDUPVAL:
use(inst.a);
break;
default:
// All instructions which reference registers have to be handled explicitly
LUAU_ASSERT(inst.a.kind != IrOpKind::VmReg);

View file

@ -452,7 +452,7 @@ void IrBuilder::translateInst(LuauOpcode op, const Instruction* pc, int i)
inst(IrCmd::FALLBACK_GETVARARGS, constUint(i), vmReg(LUAU_INSN_A(*pc)), constInt(LUAU_INSN_B(*pc) - 1));
break;
case LOP_NEWCLOSURE:
inst(IrCmd::FALLBACK_NEWCLOSURE, constUint(i), vmReg(LUAU_INSN_A(*pc)), constUint(LUAU_INSN_D(*pc)));
translateInstNewClosure(*this, pc, i);
break;
case LOP_DUPCLOSURE:
inst(IrCmd::FALLBACK_DUPCLOSURE, constUint(i), vmReg(LUAU_INSN_A(*pc)), vmConst(LUAU_INSN_D(*pc)));

View file

@ -61,6 +61,12 @@ static const char* getTagName(uint8_t tag)
return "tuserdata";
case LUA_TTHREAD:
return "tthread";
case LUA_TPROTO:
return "tproto";
case LUA_TUPVAL:
return "tupval";
case LUA_TDEADKEY:
return "tdeadkey";
default:
LUAU_ASSERT(!"Unknown type tag");
LUAU_UNREACHABLE();
@ -93,6 +99,8 @@ const char* getCmdName(IrCmd cmd)
return "GET_SLOT_NODE_ADDR";
case IrCmd::GET_HASH_NODE_ADDR:
return "GET_HASH_NODE_ADDR";
case IrCmd::GET_CLOSURE_UPVAL_ADDR:
return "GET_CLOSURE_UPVAL_ADDR";
case IrCmd::STORE_TAG:
return "STORE_TAG";
case IrCmd::STORE_POINTER:
@ -267,8 +275,8 @@ const char* getCmdName(IrCmd cmd)
return "FALLBACK_PREPVARARGS";
case IrCmd::FALLBACK_GETVARARGS:
return "FALLBACK_GETVARARGS";
case IrCmd::FALLBACK_NEWCLOSURE:
return "FALLBACK_NEWCLOSURE";
case IrCmd::NEWCLOSURE:
return "NEWCLOSURE";
case IrCmd::FALLBACK_DUPCLOSURE:
return "FALLBACK_DUPCLOSURE";
case IrCmd::FALLBACK_FORGPREP:
@ -303,6 +311,8 @@ const char* getCmdName(IrCmd cmd)
return "GET_TYPE";
case IrCmd::GET_TYPEOF:
return "GET_TYPEOF";
case IrCmd::FINDUPVAL:
return "FINDUPVAL";
}
LUAU_UNREACHABLE();

View file

@ -314,6 +314,14 @@ void IrLoweringA64::lowerInst(IrInst& inst, uint32_t index, IrBlock& next)
build.add(inst.regA64, inst.regA64, zextReg(temp2), kLuaNodeSizeLog2);
break;
}
case IrCmd::GET_CLOSURE_UPVAL_ADDR:
{
inst.regA64 = regs.allocReuse(KindA64::x, index, {inst.a});
RegisterA64 cl = inst.a.kind == IrOpKind::Undef ? rClosure : regOp(inst.a);
build.add(inst.regA64, cl, uint16_t(offsetof(Closure, l.uprefs) + sizeof(TValue) * vmUpvalueOp(inst.b)));
break;
}
case IrCmd::STORE_TAG:
{
RegisterA64 temp = regs.allocTemp(KindA64::w);
@ -1044,13 +1052,20 @@ void IrLoweringA64::lowerInst(IrInst& inst, uint32_t index, IrBlock& next)
bool continueInVm = (inst.d.kind == IrOpKind::Constant && intOp(inst.d));
Label fresh; // used when guard aborts execution or jumps to a VM exit
Label& fail = continueInVm ? helpers.exitContinueVmClearNativeFlag : getTargetLabel(inst.c, fresh);
// To support DebugLuauAbortingChecks, CHECK_TAG with VmReg has to be handled
RegisterA64 tag = inst.a.kind == IrOpKind::VmReg ? regs.allocTemp(KindA64::w) : regOp(inst.a);
if (inst.a.kind == IrOpKind::VmReg)
build.ldr(tag, mem(rBase, vmRegOp(inst.a) * sizeof(TValue) + offsetof(TValue, tt)));
if (tagOp(inst.b) == 0)
{
build.cbnz(regOp(inst.a), fail);
build.cbnz(tag, fail);
}
else
{
build.cmp(regOp(inst.a), tagOp(inst.b));
build.cmp(tag, tagOp(inst.b));
build.b(ConditionA64::NotEqual, fail);
}
if (!continueInVm)
@ -1517,13 +1532,26 @@ void IrLoweringA64::lowerInst(IrInst& inst, uint32_t index, IrBlock& next)
regs.spill(build, index);
emitFallback(build, offsetof(NativeContext, executeGETVARARGS), uintOp(inst.a));
break;
case IrCmd::FALLBACK_NEWCLOSURE:
LUAU_ASSERT(inst.b.kind == IrOpKind::VmReg);
LUAU_ASSERT(inst.c.kind == IrOpKind::Constant);
case IrCmd::NEWCLOSURE:
{
RegisterA64 reg = regOp(inst.b); // note: we need to call regOp before spill so that we don't do redundant reloads
regs.spill(build, index);
emitFallback(build, offsetof(NativeContext, executeNEWCLOSURE), uintOp(inst.a));
regs.spill(build, index, {reg});
build.mov(x2, reg);
build.mov(x0, rState);
build.mov(w1, uintOp(inst.a));
build.ldr(x3, mem(rClosure, offsetof(Closure, l.p)));
build.ldr(x3, mem(x3, offsetof(Proto, p)));
build.ldr(x3, mem(x3, sizeof(Proto*) * uintOp(inst.c)));
build.ldr(x4, mem(rNativeContext, offsetof(NativeContext, luaF_newLclosure)));
build.blr(x4);
inst.regA64 = regs.takeReg(x0, index);
break;
}
case IrCmd::FALLBACK_DUPCLOSURE:
LUAU_ASSERT(inst.b.kind == IrOpKind::VmReg);
LUAU_ASSERT(inst.c.kind == IrOpKind::VmConst);
@ -1743,6 +1771,18 @@ void IrLoweringA64::lowerInst(IrInst& inst, uint32_t index, IrBlock& next)
break;
}
case IrCmd::FINDUPVAL:
{
regs.spill(build, index);
build.mov(x0, rState);
build.add(x1, rBase, uint16_t(vmRegOp(inst.a) * sizeof(TValue)));
build.ldr(x2, mem(rNativeContext, offsetof(NativeContext, luaF_findupval)));
build.blr(x2);
inst.regA64 = regs.takeReg(x0, index);
break;
}
// To handle unsupported instructions, add "case IrCmd::OP" and make sure to set error = true!
}

View file

@ -186,14 +186,40 @@ void IrLoweringX64::lowerInst(IrInst& inst, uint32_t index, IrBlock& next)
build.add(inst.regX64, tmp.reg);
break;
};
case IrCmd::GET_CLOSURE_UPVAL_ADDR:
{
inst.regX64 = regs.allocRegOrReuse(SizeX64::qword, index, {inst.a});
if (inst.a.kind == IrOpKind::Undef)
{
build.mov(inst.regX64, sClosure);
}
else
{
RegisterX64 cl = regOp(inst.a);
if (inst.regX64 != cl)
build.mov(inst.regX64, cl);
}
build.add(inst.regX64, offsetof(Closure, l.uprefs) + sizeof(TValue) * vmUpvalueOp(inst.b));
break;
}
case IrCmd::STORE_TAG:
if (inst.b.kind == IrOpKind::Constant)
build.mov(luauRegTag(vmRegOp(inst.a)), tagOp(inst.b));
{
if (inst.a.kind == IrOpKind::Inst)
build.mov(dword[regOp(inst.a) + offsetof(TValue, tt)], tagOp(inst.b));
else
build.mov(luauRegTag(vmRegOp(inst.a)), tagOp(inst.b));
}
else
LUAU_ASSERT(!"Unsupported instruction form");
break;
case IrCmd::STORE_POINTER:
build.mov(luauRegValue(vmRegOp(inst.a)), regOp(inst.b));
if (inst.a.kind == IrOpKind::Inst)
build.mov(qword[regOp(inst.a) + offsetof(TValue, value)], regOp(inst.b));
else
build.mov(luauRegValue(vmRegOp(inst.a)), regOp(inst.b));
break;
case IrCmd::STORE_DOUBLE:
if (inst.b.kind == IrOpKind::Constant)
@ -1207,12 +1233,25 @@ void IrLoweringX64::lowerInst(IrInst& inst, uint32_t index, IrBlock& next)
emitFallback(regs, build, offsetof(NativeContext, executeGETVARARGS), uintOp(inst.a));
break;
case IrCmd::FALLBACK_NEWCLOSURE:
LUAU_ASSERT(inst.b.kind == IrOpKind::VmReg);
LUAU_ASSERT(inst.c.kind == IrOpKind::Constant);
case IrCmd::NEWCLOSURE:
{
ScopedRegX64 tmp2{regs, SizeX64::qword};
build.mov(tmp2.reg, sClosure);
build.mov(tmp2.reg, qword[tmp2.reg + offsetof(Closure, l.p)]);
build.mov(tmp2.reg, qword[tmp2.reg + offsetof(Proto, p)]);
build.mov(tmp2.reg, qword[tmp2.reg + sizeof(Proto*) * uintOp(inst.c)]);
emitFallback(regs, build, offsetof(NativeContext, executeNEWCLOSURE), uintOp(inst.a));
IrCallWrapperX64 callWrap(regs, build, index);
callWrap.addArgument(SizeX64::qword, rState);
callWrap.addArgument(SizeX64::dword, uintOp(inst.a), inst.a);
callWrap.addArgument(SizeX64::qword, regOp(inst.b), inst.b);
callWrap.addArgument(SizeX64::qword, tmp2);
callWrap.call(qword[rNativeContext + offsetof(NativeContext, luaF_newLclosure)]);
inst.regX64 = regs.takeReg(rax, index);
break;
}
case IrCmd::FALLBACK_DUPCLOSURE:
LUAU_ASSERT(inst.b.kind == IrOpKind::VmReg);
LUAU_ASSERT(inst.c.kind == IrOpKind::VmConst);
@ -1412,6 +1451,17 @@ void IrLoweringX64::lowerInst(IrInst& inst, uint32_t index, IrBlock& next)
break;
}
case IrCmd::FINDUPVAL:
{
IrCallWrapperX64 callWrap(regs, build);
callWrap.addArgument(SizeX64::qword, rState);
callWrap.addArgument(SizeX64::qword, luauRegAddress(vmRegOp(inst.a)));
callWrap.call(qword[rNativeContext + offsetof(NativeContext, luaF_findupval)]);
inst.regX64 = regs.takeReg(rax, index);
break;
}
// Pseudo instructions
case IrCmd::NOP:
case IrCmd::SUBSTITUTE:

View file

@ -1213,5 +1213,61 @@ void translateInstOrX(IrBuilder& build, const Instruction* pc, int pcpos, IrOp c
}
}
void translateInstNewClosure(IrBuilder& build, const Instruction* pc, int pcpos)
{
LUAU_ASSERT(unsigned(LUAU_INSN_D(*pc)) < unsigned(build.function.proto->sizep));
int ra = LUAU_INSN_A(*pc);
Proto* pv = build.function.proto->p[LUAU_INSN_D(*pc)];
build.inst(IrCmd::SET_SAVEDPC, build.constUint(pcpos + 1));
IrOp env = build.inst(IrCmd::LOAD_ENV);
IrOp ncl = build.inst(IrCmd::NEWCLOSURE, build.constUint(pv->nups), env, build.constUint(LUAU_INSN_D(*pc)));
build.inst(IrCmd::STORE_POINTER, build.vmReg(ra), ncl);
build.inst(IrCmd::STORE_TAG, build.vmReg(ra), build.constTag(LUA_TFUNCTION));
for (int ui = 0; ui < pv->nups; ++ui)
{
Instruction uinsn = pc[ui + 1];
LUAU_ASSERT(LUAU_INSN_OP(uinsn) == LOP_CAPTURE);
IrOp dst = build.inst(IrCmd::GET_CLOSURE_UPVAL_ADDR, ncl, build.vmUpvalue(ui));
switch (LUAU_INSN_A(uinsn))
{
case LCT_VAL:
{
IrOp src = build.inst(IrCmd::LOAD_TVALUE, build.vmReg(LUAU_INSN_B(uinsn)));
build.inst(IrCmd::STORE_TVALUE, dst, src);
break;
}
case LCT_REF:
{
IrOp src = build.inst(IrCmd::FINDUPVAL, build.vmReg(LUAU_INSN_B(uinsn)));
build.inst(IrCmd::STORE_POINTER, dst, src);
build.inst(IrCmd::STORE_TAG, dst, build.constTag(LUA_TUPVAL));
break;
}
case LCT_UPVAL:
{
IrOp src = build.inst(IrCmd::GET_CLOSURE_UPVAL_ADDR, build.undef(), build.vmUpvalue(LUAU_INSN_B(uinsn)));
IrOp load = build.inst(IrCmd::LOAD_TVALUE, src);
build.inst(IrCmd::STORE_TVALUE, dst, load);
break;
}
default:
LUAU_ASSERT(!"Unknown upvalue capture type");
LUAU_UNREACHABLE(); // improves switch() codegen by eliding opcode bounds checks
}
}
build.inst(IrCmd::CHECK_GC);
}
} // namespace CodeGen
} // namespace Luau

View file

@ -65,6 +65,7 @@ void translateInstCapture(IrBuilder& build, const Instruction* pc, int pcpos);
void translateInstNamecall(IrBuilder& build, const Instruction* pc, int pcpos);
void translateInstAndX(IrBuilder& build, const Instruction* pc, int pcpos, IrOp c);
void translateInstOrX(IrBuilder& build, const Instruction* pc, int pcpos, IrOp c);
void translateInstNewClosure(IrBuilder& build, const Instruction* pc, int pcpos);
inline int getOpLength(LuauOpcode op)
{

View file

@ -38,6 +38,7 @@ IrValueKind getCmdValueKind(IrCmd cmd)
case IrCmd::GET_ARR_ADDR:
case IrCmd::GET_SLOT_NODE_ADDR:
case IrCmd::GET_HASH_NODE_ADDR:
case IrCmd::GET_CLOSURE_UPVAL_ADDR:
return IrValueKind::Pointer;
case IrCmd::STORE_TAG:
case IrCmd::STORE_POINTER:
@ -141,7 +142,9 @@ IrValueKind getCmdValueKind(IrCmd cmd)
case IrCmd::FALLBACK_NAMECALL:
case IrCmd::FALLBACK_PREPVARARGS:
case IrCmd::FALLBACK_GETVARARGS:
case IrCmd::FALLBACK_NEWCLOSURE:
return IrValueKind::None;
case IrCmd::NEWCLOSURE:
return IrValueKind::Pointer;
case IrCmd::FALLBACK_DUPCLOSURE:
case IrCmd::FALLBACK_FORGPREP:
return IrValueKind::None;
@ -164,6 +167,8 @@ IrValueKind getCmdValueKind(IrCmd cmd)
case IrCmd::GET_TYPE:
case IrCmd::GET_TYPEOF:
return IrValueKind::Pointer;
case IrCmd::FINDUPVAL:
return IrValueKind::Pointer;
}
LUAU_UNREACHABLE();

View file

@ -77,7 +77,6 @@ void IrValueLocationTracking::beforeInstLowering(IrInst& inst)
case IrCmd::FALLBACK_GETVARARGS:
invalidateRestoreVmRegs(vmRegOp(inst.b), function.intOp(inst.c));
break;
case IrCmd::FALLBACK_NEWCLOSURE:
case IrCmd::FALLBACK_DUPCLOSURE:
invalidateRestoreOp(inst.b);
break;
@ -109,6 +108,8 @@ void IrValueLocationTracking::beforeInstLowering(IrInst& inst)
case IrCmd::FALLBACK_PREPVARARGS:
case IrCmd::ADJUST_STACK_TO_TOP:
case IrCmd::GET_TYPEOF:
case IrCmd::NEWCLOSURE:
case IrCmd::FINDUPVAL:
break;
// These instrucitons read VmReg only after optimizeMemoryOperandsX64

View file

@ -56,6 +56,8 @@ void initFunctions(NativeState& data)
data.context.luaC_step = luaC_step;
data.context.luaF_close = luaF_close;
data.context.luaF_findupval = luaF_findupval;
data.context.luaF_newLclosure = luaF_newLclosure;
data.context.luaT_gettm = luaT_gettm;
data.context.luaT_objtypenamestr = luaT_objtypenamestr;

View file

@ -52,6 +52,8 @@ struct NativeContext
size_t (*luaC_step)(lua_State* L, bool assist) = nullptr;
void (*luaF_close)(lua_State* L, StkId level) = nullptr;
UpVal* (*luaF_findupval)(lua_State* L, StkId level) = nullptr;
Closure* (*luaF_newLclosure)(lua_State* L, int nelems, Table* e, Proto* p) = nullptr;
const TValue* (*luaT_gettm)(Table* events, TMS event, TString* ename) = nullptr;
const TString* (*luaT_objtypenamestr)(lua_State* L, const TValue* o) = nullptr;

View file

@ -415,6 +415,8 @@ static void handleBuiltinEffects(ConstPropState& state, LuauBuiltinFunction bfid
case LBF_RAWLEN:
case LBF_BIT32_EXTRACTK:
case LBF_GETMETATABLE:
case LBF_TONUMBER:
case LBF_TOSTRING:
break;
case LBF_SETMETATABLE:
state.invalidateHeap(); // TODO: only knownNoMetatable is affected and we might know which one
@ -760,6 +762,7 @@ static void constPropInInst(ConstPropState& state, IrBuilder& build, IrFunction&
case IrCmd::GET_ARR_ADDR:
case IrCmd::GET_SLOT_NODE_ADDR:
case IrCmd::GET_HASH_NODE_ADDR:
case IrCmd::GET_CLOSURE_UPVAL_ADDR:
break;
case IrCmd::ADD_INT:
case IrCmd::SUB_INT:
@ -823,6 +826,7 @@ static void constPropInInst(ConstPropState& state, IrBuilder& build, IrFunction&
case IrCmd::BITXOR_UINT:
case IrCmd::BITOR_UINT:
case IrCmd::BITNOT_UINT:
break;
case IrCmd::BITLSHIFT_UINT:
case IrCmd::BITRSHIFT_UINT:
case IrCmd::BITARSHIFT_UINT:
@ -833,6 +837,7 @@ static void constPropInInst(ConstPropState& state, IrBuilder& build, IrFunction&
case IrCmd::INVOKE_LIBM:
case IrCmd::GET_TYPE:
case IrCmd::GET_TYPEOF:
case IrCmd::FINDUPVAL:
break;
case IrCmd::JUMP_CMP_ANY:
@ -923,8 +928,7 @@ static void constPropInInst(ConstPropState& state, IrBuilder& build, IrFunction&
case IrCmd::FALLBACK_GETVARARGS:
state.invalidateRegisterRange(vmRegOp(inst.b), function.intOp(inst.c));
break;
case IrCmd::FALLBACK_NEWCLOSURE:
state.invalidate(inst.b);
case IrCmd::NEWCLOSURE:
break;
case IrCmd::FALLBACK_DUPCLOSURE:
state.invalidate(inst.b);

View file

@ -44,6 +44,7 @@
// Version 1: Baseline version for the open-source release. Supported until 0.521.
// Version 2: Adds Proto::linedefined. Supported until 0.544.
// Version 3: Adds FORGPREP/JUMPXEQK* and enhances AUX encoding for FORGLOOP. Removes FORGLOOP_NEXT/INEXT and JUMPIFEQK/JUMPIFNOTEQK. Currently supported.
// Version 4: Adds Proto::flags and typeinfo. Currently supported.
// Bytecode opcode, part of the instruction header
enum LuauOpcode
@ -543,6 +544,10 @@ enum LuauBuiltinFunction
// get/setmetatable
LBF_GETMETATABLE,
LBF_SETMETATABLE,
// tonumber/tostring
LBF_TONUMBER,
LBF_TOSTRING,
};
// Capture type, used in LOP_CAPTURE
@ -552,3 +557,10 @@ enum LuauCaptureType
LCT_REF,
LCT_UPVAL,
};
// Proto flag bitmask, stored in Proto::flags
enum LuauProtoFlag
{
// used to tag main proto for modules with --!native
LPF_NATIVE_MODULE = 1 << 0,
};

View file

@ -4,6 +4,9 @@
#include "Luau/Bytecode.h"
#include "Luau/Compiler.h"
LUAU_FASTFLAGVARIABLE(LuauCompileBuiltinTonumber, false)
LUAU_FASTFLAGVARIABLE(LuauCompileBuiltinTostring, false)
namespace Luau
{
namespace Compile
@ -69,6 +72,11 @@ static int getBuiltinFunctionId(const Builtin& builtin, const CompileOptions& op
if (builtin.isGlobal("setmetatable"))
return LBF_SETMETATABLE;
if (FFlag::LuauCompileBuiltinTonumber && builtin.isGlobal("tonumber"))
return LBF_TONUMBER;
if (FFlag::LuauCompileBuiltinTostring && builtin.isGlobal("tostring"))
return LBF_TOSTRING;
if (builtin.object == "math")
{
if (builtin.method == "abs")
@ -257,10 +265,10 @@ BuiltinInfo getBuiltinInfo(int bfid)
case LBF_MATH_ABS:
case LBF_MATH_ACOS:
case LBF_MATH_ASIN:
return {1, 1};
return {1, 1, BuiltinInfo::Flag_NoneSafe};
case LBF_MATH_ATAN2:
return {2, 1};
return {2, 1, BuiltinInfo::Flag_NoneSafe};
case LBF_MATH_ATAN:
case LBF_MATH_CEIL:
@ -269,19 +277,19 @@ BuiltinInfo getBuiltinInfo(int bfid)
case LBF_MATH_DEG:
case LBF_MATH_EXP:
case LBF_MATH_FLOOR:
return {1, 1};
return {1, 1, BuiltinInfo::Flag_NoneSafe};
case LBF_MATH_FMOD:
return {2, 1};
return {2, 1, BuiltinInfo::Flag_NoneSafe};
case LBF_MATH_FREXP:
return {1, 2};
return {1, 2, BuiltinInfo::Flag_NoneSafe};
case LBF_MATH_LDEXP:
return {2, 1};
return {2, 1, BuiltinInfo::Flag_NoneSafe};
case LBF_MATH_LOG10:
return {1, 1};
return {1, 1, BuiltinInfo::Flag_NoneSafe};
case LBF_MATH_LOG:
return {-1, 1}; // 1 or 2 parameters
@ -291,10 +299,10 @@ BuiltinInfo getBuiltinInfo(int bfid)
return {-1, 1}; // variadic
case LBF_MATH_MODF:
return {1, 2};
return {1, 2, BuiltinInfo::Flag_NoneSafe};
case LBF_MATH_POW:
return {2, 1};
return {2, 1, BuiltinInfo::Flag_NoneSafe};
case LBF_MATH_RAD:
case LBF_MATH_SINH:
@ -302,16 +310,16 @@ BuiltinInfo getBuiltinInfo(int bfid)
case LBF_MATH_SQRT:
case LBF_MATH_TANH:
case LBF_MATH_TAN:
return {1, 1};
return {1, 1, BuiltinInfo::Flag_NoneSafe};
case LBF_BIT32_ARSHIFT:
return {2, 1};
return {2, 1, BuiltinInfo::Flag_NoneSafe};
case LBF_BIT32_BAND:
return {-1, 1}; // variadic
case LBF_BIT32_BNOT:
return {1, 1};
return {1, 1, BuiltinInfo::Flag_NoneSafe};
case LBF_BIT32_BOR:
case LBF_BIT32_BXOR:
@ -323,14 +331,14 @@ BuiltinInfo getBuiltinInfo(int bfid)
case LBF_BIT32_LROTATE:
case LBF_BIT32_LSHIFT:
return {2, 1};
return {2, 1, BuiltinInfo::Flag_NoneSafe};
case LBF_BIT32_REPLACE:
return {-1, 1}; // 3 or 4 parameters
case LBF_BIT32_RROTATE:
case LBF_BIT32_RSHIFT:
return {2, 1};
return {2, 1, BuiltinInfo::Flag_NoneSafe};
case LBF_TYPE:
return {1, 1};
@ -342,7 +350,7 @@ BuiltinInfo getBuiltinInfo(int bfid)
return {-1, 1}; // variadic
case LBF_STRING_LEN:
return {1, 1};
return {1, 1, BuiltinInfo::Flag_NoneSafe};
case LBF_TYPEOF:
return {1, 1};
@ -351,11 +359,11 @@ BuiltinInfo getBuiltinInfo(int bfid)
return {-1, 1}; // 2 or 3 parameters
case LBF_MATH_CLAMP:
return {3, 1};
return {3, 1, BuiltinInfo::Flag_NoneSafe};
case LBF_MATH_SIGN:
case LBF_MATH_ROUND:
return {1, 1};
return {1, 1, BuiltinInfo::Flag_NoneSafe};
case LBF_RAWSET:
return {3, 1};
@ -375,22 +383,28 @@ BuiltinInfo getBuiltinInfo(int bfid)
case LBF_BIT32_COUNTLZ:
case LBF_BIT32_COUNTRZ:
return {1, 1};
return {1, 1, BuiltinInfo::Flag_NoneSafe};
case LBF_SELECT_VARARG:
return {-1, -1}; // variadic
case LBF_RAWLEN:
return {1, 1};
return {1, 1, BuiltinInfo::Flag_NoneSafe};
case LBF_BIT32_EXTRACTK:
return {3, 1};
return {3, 1, BuiltinInfo::Flag_NoneSafe};
case LBF_GETMETATABLE:
return {1, 1};
case LBF_SETMETATABLE:
return {2, 1};
case LBF_TONUMBER:
return {-1, 1}; // 1 or 2 parameters
case LBF_TOSTRING:
return {1, 1};
};
LUAU_UNREACHABLE();

View file

@ -41,8 +41,17 @@ void analyzeBuiltins(DenseHashMap<AstExprCall*, int>& result, const DenseHashMap
struct BuiltinInfo
{
enum Flags
{
// none-safe builtins are builtins that have the same behavior for arguments that are nil or none
// this allows the compiler to compile calls to builtins more efficiently in certain cases
// for example, math.abs(x()) may compile x() as if it returns one value; if it returns no values, abs() will get nil instead of none
Flag_NoneSafe = 1 << 0,
};
int params;
int results;
unsigned int flags;
};
BuiltinInfo getBuiltinInfo(int bfid);

View file

@ -27,6 +27,9 @@ LUAU_FASTINTVARIABLE(LuauCompileInlineThresholdMaxBoost, 300)
LUAU_FASTINTVARIABLE(LuauCompileInlineDepth, 5)
LUAU_FASTFLAGVARIABLE(LuauCompileFunctionType, false)
LUAU_FASTFLAGVARIABLE(LuauCompileNativeComment, false)
LUAU_FASTFLAGVARIABLE(LuauCompileFixBuiltinArity, false)
namespace Luau
{
@ -187,7 +190,7 @@ struct Compiler
return node->as<AstExprFunction>();
}
uint32_t compileFunction(AstExprFunction* func)
uint32_t compileFunction(AstExprFunction* func, uint8_t protoflags)
{
LUAU_TIMETRACE_SCOPE("Compiler::compileFunction", "Compiler");
@ -262,7 +265,7 @@ struct Compiler
if (bytecode.getInstructionCount() > kMaxInstructionCount)
CompileError::raise(func->location, "Exceeded function instruction limit; split the function into parts to compile");
bytecode.endFunction(uint8_t(stackSize), uint8_t(upvals.size()));
bytecode.endFunction(uint8_t(stackSize), uint8_t(upvals.size()), protoflags);
Function& f = functions[func];
f.id = fid;
@ -792,8 +795,19 @@ struct Compiler
{
if (!isExprMultRet(expr->args.data[expr->args.size - 1]))
return compileExprFastcallN(expr, target, targetCount, targetTop, multRet, regs, bfid);
else if (options.optimizationLevel >= 2 && int(expr->args.size) == getBuiltinInfo(bfid).params)
return compileExprFastcallN(expr, target, targetCount, targetTop, multRet, regs, bfid);
else if (options.optimizationLevel >= 2)
{
if (FFlag::LuauCompileFixBuiltinArity)
{
// when a builtin is none-safe with matching arity, even if the last expression returns 0 or >1 arguments,
// we can rely on the behavior of the function being the same (none-safe means nil and none are interchangeable)
BuiltinInfo info = getBuiltinInfo(bfid);
if (int(expr->args.size) == info.params && (info.flags & BuiltinInfo::Flag_NoneSafe) != 0)
return compileExprFastcallN(expr, target, targetCount, targetTop, multRet, regs, bfid);
}
else if (int(expr->args.size) == getBuiltinInfo(bfid).params)
return compileExprFastcallN(expr, target, targetCount, targetTop, multRet, regs, bfid);
}
}
if (expr->self)
@ -3147,7 +3161,7 @@ struct Compiler
}
}
// compute expressions with side effects for lulz
// compute expressions with side effects
for (size_t i = stat->vars.size; i < stat->values.size; ++i)
{
RegScope rsi(this);
@ -3834,11 +3848,20 @@ void compileOrThrow(BytecodeBuilder& bytecode, const ParseResult& parseResult, c
LUAU_ASSERT(parseResult.errors.empty());
CompileOptions options = inputOptions;
uint8_t mainFlags = 0;
for (const HotComment& hc : parseResult.hotcomments)
{
if (hc.header && hc.content.compare(0, 9, "optimize ") == 0)
options.optimizationLevel = std::max(0, std::min(2, atoi(hc.content.c_str() + 9)));
if (FFlag::LuauCompileNativeComment && hc.header && hc.content == "native")
{
mainFlags |= LPF_NATIVE_MODULE;
options.optimizationLevel = 2; // note: this might be removed in the future in favor of --!optimize
}
}
AstStatBlock* root = parseResult.root;
Compiler compiler(bytecode, options);
@ -3884,12 +3907,12 @@ void compileOrThrow(BytecodeBuilder& bytecode, const ParseResult& parseResult, c
root->visit(&functionVisitor);
for (AstExprFunction* expr : functions)
compiler.compileFunction(expr);
compiler.compileFunction(expr, 0);
AstExprFunction main(root->location, /*generics= */ AstArray<AstGenericType>(), /*genericPacks= */ AstArray<AstGenericTypePack>(),
/* self= */ nullptr, AstArray<AstLocal*>(), /* vararg= */ true, /* varargLocation= */ Luau::Location(), root, /* functionDepth= */ 0,
/* debugname= */ AstName());
uint32_t mainid = compiler.compileFunction(&main);
uint32_t mainid = compiler.compileFunction(&main, mainFlags);
const Compiler::Function* mainf = compiler.functions.find(&main);
LUAU_ASSERT(mainf && mainf->upvals.empty());

View file

@ -6,6 +6,8 @@
#include <limits.h>
LUAU_FASTFLAGVARIABLE(LuauAssignmentHasCost, false)
namespace Luau
{
namespace Compile
@ -302,12 +304,14 @@ struct CostVisitor : AstVisitor
return false;
}
bool visit(AstStat* node) override
bool visit(AstStatIf* node) override
{
if (node->is<AstStatIf>())
// unconditional 'else' may require a jump after the 'if' body
// note: this ignores cases when 'then' always terminates and also assumes comparison requires an extra instruction which may be false
if (!FFlag::LuauAssignmentHasCost)
result += 2;
else if (node->is<AstStatBreak>() || node->is<AstStatContinue>())
result += 1;
else
result += 1 + (node->elsebody && !node->elsebody->is<AstStatIf>());
return true;
}
@ -333,7 +337,21 @@ struct CostVisitor : AstVisitor
for (size_t i = 0; i < node->vars.size; ++i)
assign(node->vars.data[i]);
return true;
if (!FFlag::LuauAssignmentHasCost)
return true;
for (size_t i = 0; i < node->vars.size || i < node->values.size; ++i)
{
Cost ac;
if (i < node->vars.size)
ac += model(node->vars.data[i]);
if (i < node->values.size)
ac += model(node->values.data[i]);
// local->local or constant->local assignment is not free
result += ac.model == 0 ? Cost(1) : ac;
}
return false;
}
bool visit(AstStatCompoundAssign* node) override
@ -345,6 +363,20 @@ struct CostVisitor : AstVisitor
return true;
}
bool visit(AstStatBreak* node) override
{
result += 1;
return false;
}
bool visit(AstStatContinue* node) override
{
result += 1;
return false;
}
};
uint64_t modelCost(AstNode* root, AstLocal* const* vars, size_t varCount, const DenseHashMap<AstExprCall*, int>& builtins)

View file

@ -186,6 +186,7 @@ target_sources(Luau.Analysis PRIVATE
Analysis/include/Luau/TypeArena.h
Analysis/include/Luau/TypeAttach.h
Analysis/include/Luau/TypeChecker2.h
Analysis/include/Luau/TypeCheckLimits.h
Analysis/include/Luau/TypedAllocator.h
Analysis/include/Luau/TypeFamily.h
Analysis/include/Luau/TypeInfer.h

View file

@ -123,7 +123,7 @@
// enables callbacks to redirect code execution from Luau VM to a custom implementation
#ifndef LUA_CUSTOM_EXECUTION
#define LUA_CUSTOM_EXECUTION 0
#define LUA_CUSTOM_EXECUTION 1
#endif
// }==================================================================

View file

@ -336,7 +336,7 @@ const char* luaL_findtable(lua_State* L, int idx, const char* fname, int szhint)
const char* luaL_typename(lua_State* L, int idx)
{
const TValue* obj = luaA_toobject(L, idx);
return luaT_objtypename(L, obj);
return obj ? luaT_objtypename(L, obj) : "no value";
}
/*

View file

@ -23,6 +23,8 @@
#endif
#endif
LUAU_DYNAMIC_FASTFLAGVARIABLE(LuauFastcallGC, false)
// luauF functions implement FASTCALL instruction that performs a direct execution of some builtin functions from the VM
// The rule of thumb is that FASTCALL functions can not call user code, yield, fail, or reallocate stack.
// If types of the arguments mismatch, luauF_* needs to return -1 and the execution will fall back to the usual call path
@ -830,6 +832,8 @@ static int luauF_char(lua_State* L, StkId res, TValue* arg0, int nresults, StkId
if (nparams < int(sizeof(buffer)) && nresults <= 1)
{
if (DFFlag::LuauFastcallGC && luaC_needsGC(L))
return -1; // we can't call luaC_checkGC so fall back to C implementation
if (nparams >= 1)
{
@ -900,6 +904,9 @@ static int luauF_sub(lua_State* L, StkId res, TValue* arg0, int nresults, StkId
int i = int(nvalue(args));
int j = int(nvalue(args + 1));
if (DFFlag::LuauFastcallGC && luaC_needsGC(L))
return -1; // we can't call luaC_checkGC so fall back to C implementation
if (i >= 1 && j >= i && unsigned(j - 1) < unsigned(ts->len))
{
setsvalue(L, res, luaS_newlstr(L, getstr(ts) + (i - 1), j - i + 1));
@ -1247,6 +1254,73 @@ static int luauF_setmetatable(lua_State* L, StkId res, TValue* arg0, int nresult
return -1;
}
static int luauF_tonumber(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams)
{
if (nparams == 1 && nresults <= 1)
{
double num;
if (ttisnumber(arg0))
{
setnvalue(res, nvalue(arg0));
return 1;
}
else if (ttisstring(arg0) && luaO_str2d(svalue(arg0), &num))
{
setnvalue(res, num);
return 1;
}
else
{
setnilvalue(res);
return 1;
}
}
return -1;
}
static int luauF_tostring(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams)
{
if (nparams >= 1 && nresults <= 1)
{
switch (ttype(arg0))
{
case LUA_TNIL:
{
TString* s = L->global->ttname[LUA_TNIL];
setsvalue(L, res, s);
return 1;
}
case LUA_TBOOLEAN:
{
TString* s = bvalue(arg0) ? luaS_newliteral(L, "true") : luaS_newliteral(L, "false");
setsvalue(L, res, s);
return 1;
}
case LUA_TNUMBER:
{
if (DFFlag::LuauFastcallGC && luaC_needsGC(L))
return -1; // we can't call luaC_checkGC so fall back to C implementation
char s[LUAI_MAXNUM2STR];
char* e = luai_num2str(s, nvalue(arg0));
setsvalue(L, res, luaS_newlstr(L, s, e - s));
return 1;
}
case LUA_TSTRING:
{
setsvalue(L, res, tsvalue(arg0));
return 1;
}
}
// fall back to generic C implementation
}
return -1;
}
static int luauF_missing(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams)
{
return -1;
@ -1411,6 +1485,9 @@ const luau_FastFunction luauF_table[256] = {
luauF_getmetatable,
luauF_setmetatable,
luauF_tonumber,
luauF_tostring,
// When adding builtins, add them above this line; what follows is 64 "dummy" entries with luauF_missing fallback.
// This is important so that older versions of the runtime that don't support newer builtins automatically fall back via luauF_missing.
// Given the builtin addition velocity this should always provide a larger compatibility window than bytecode versions suggest.

View file

@ -36,6 +36,7 @@ Proto* luaF_newproto(lua_State* L)
f->execdata = NULL;
f->exectarget = 0;
f->typeinfo = NULL;
f->userdata = NULL;
return f;
}

View file

@ -73,10 +73,12 @@
#define luaC_white(g) cast_to(uint8_t, ((g)->currentwhite) & WHITEBITS)
#define luaC_needsGC(L) (L->global->totalbytes >= L->global->GCthreshold)
#define luaC_checkGC(L) \
{ \
condhardstacktests(luaD_reallocstack(L, L->stacksize - EXTRA_STACK)); \
if (L->global->totalbytes >= L->global->GCthreshold) \
if (luaC_needsGC(L)) \
{ \
condhardmemtests(luaC_validate(L), 1); \
luaC_step(L, true); \

View file

@ -7,6 +7,8 @@
#include <math.h>
#include <time.h>
LUAU_FASTFLAGVARIABLE(LuauFasterNoise, false)
#undef PI
#define PI (3.14159265358979323846)
#define RADIANS_PER_DEGREE (PI / 180.0)
@ -275,6 +277,7 @@ static int math_randomseed(lua_State* L)
return 0;
}
// TODO: Delete with LuauFasterNoise
static const unsigned char kPerlin[512] = {151, 160, 137, 91, 90, 15, 131, 13, 201, 95, 96, 53, 194, 233, 7, 225, 140, 36, 103, 30, 69, 142, 8, 99,
37, 240, 21, 10, 23, 190, 6, 148, 247, 120, 234, 75, 0, 26, 197, 62, 94, 252, 219, 203, 117, 35, 11, 32, 57, 177, 33, 88, 237, 149, 56, 87, 174,
20, 125, 136, 171, 168, 68, 175, 74, 165, 71, 134, 139, 48, 27, 166, 77, 146, 158, 231, 83, 111, 229, 122, 60, 211, 133, 230, 220, 105, 92, 41,
@ -295,18 +298,32 @@ static const unsigned char kPerlin[512] = {151, 160, 137, 91, 90, 15, 131, 13, 2
106, 157, 184, 84, 204, 176, 115, 121, 50, 45, 127, 4, 150, 254, 138, 236, 205, 93, 222, 114, 67, 29, 24, 72, 243, 141, 128, 195, 78, 66, 215, 61,
156, 180};
static float fade(float t)
static const unsigned char kPerlinHash[257] = {151, 160, 137, 91, 90, 15, 131, 13, 201, 95, 96, 53, 194, 233, 7, 225, 140, 36, 103, 30, 69, 142, 8, 99,
37, 240, 21, 10, 23, 190, 6, 148, 247, 120, 234, 75, 0, 26, 197, 62, 94, 252, 219, 203, 117, 35, 11, 32, 57, 177, 33, 88, 237, 149, 56, 87, 174,
20, 125, 136, 171, 168, 68, 175, 74, 165, 71, 134, 139, 48, 27, 166, 77, 146, 158, 231, 83, 111, 229, 122, 60, 211, 133, 230, 220, 105, 92, 41,
55, 46, 245, 40, 244, 102, 143, 54, 65, 25, 63, 161, 1, 216, 80, 73, 209, 76, 132, 187, 208, 89, 18, 169, 200, 196, 135, 130, 116, 188, 159, 86,
164, 100, 109, 198, 173, 186, 3, 64, 52, 217, 226, 250, 124, 123, 5, 202, 38, 147, 118, 126, 255, 82, 85, 212, 207, 206, 59, 227, 47, 16, 58, 17,
182, 189, 28, 42, 223, 183, 170, 213, 119, 248, 152, 2, 44, 154, 163, 70, 221, 153, 101, 155, 167, 43, 172, 9, 129, 22, 39, 253, 19, 98, 108, 110,
79, 113, 224, 232, 178, 185, 112, 104, 218, 246, 97, 228, 251, 34, 242, 193, 238, 210, 144, 12, 191, 179, 162, 241, 81, 51, 145, 235, 249, 14,
239, 107, 49, 192, 214, 31, 181, 199, 106, 157, 184, 84, 204, 176, 115, 121, 50, 45, 127, 4, 150, 254, 138, 236, 205, 93, 222, 114, 67, 29, 24,
72, 243, 141, 128, 195, 78, 66, 215, 61, 156, 180, 151};
const float kPerlinGrad[16][3] = {{1, 1, 0}, {-1, 1, 0}, {1, -1, 0}, {-1, -1, 0}, {1, 0, 1}, {-1, 0, 1}, {1, 0, -1}, {-1, 0, -1}, {0, 1, 1},
{0, -1, 1}, {0, 1, -1}, {0, -1, -1}, {1, 1, 0}, {0, -1, 1}, {-1, 1, 0}, {0, -1, -1}};
static float perlin_fade(float t)
{
return t * t * t * (t * (t * 6 - 15) + 10);
}
static float math_lerp(float t, float a, float b)
static float perlin_lerp(float t, float a, float b)
{
return a + t * (b - a);
}
static float grad(unsigned char hash, float x, float y, float z)
{
LUAU_ASSERT(!FFlag::LuauFasterNoise);
unsigned char h = hash & 15;
float u = (h < 8) ? x : y;
float v = (h < 4) ? y : (h == 12 || h == 14) ? x : z;
@ -314,8 +331,15 @@ static float grad(unsigned char hash, float x, float y, float z)
return (h & 1 ? -u : u) + (h & 2 ? -v : v);
}
static float perlin(float x, float y, float z)
static float perlin_grad(int hash, float x, float y, float z)
{
const float* g = kPerlinGrad[hash & 15];
return g[0] * x + g[1] * y + g[2] * z;
}
static float perlin_dep(float x, float y, float z)
{
LUAU_ASSERT(!FFlag::LuauFasterNoise);
float xflr = floorf(x);
float yflr = floorf(y);
float zflr = floorf(z);
@ -328,9 +352,9 @@ static float perlin(float x, float y, float z)
float yf = y - yflr;
float zf = z - zflr;
float u = fade(xf);
float v = fade(yf);
float w = fade(zf);
float u = perlin_fade(xf);
float v = perlin_fade(yf);
float w = perlin_fade(zf);
const unsigned char* p = kPerlin;
@ -342,24 +366,79 @@ static float perlin(float x, float y, float z)
int ba = p[b] + zi;
int bb = p[b + 1] + zi;
return math_lerp(w,
math_lerp(v, math_lerp(u, grad(p[aa], xf, yf, zf), grad(p[ba], xf - 1, yf, zf)),
math_lerp(u, grad(p[ab], xf, yf - 1, zf), grad(p[bb], xf - 1, yf - 1, zf))),
math_lerp(v, math_lerp(u, grad(p[aa + 1], xf, yf, zf - 1), grad(p[ba + 1], xf - 1, yf, zf - 1)),
math_lerp(u, grad(p[ab + 1], xf, yf - 1, zf - 1), grad(p[bb + 1], xf - 1, yf - 1, zf - 1))));
return perlin_lerp(w,
perlin_lerp(v, perlin_lerp(u, grad(p[aa], xf, yf, zf), grad(p[ba], xf - 1, yf, zf)),
perlin_lerp(u, grad(p[ab], xf, yf - 1, zf), grad(p[bb], xf - 1, yf - 1, zf))),
perlin_lerp(v, perlin_lerp(u, grad(p[aa + 1], xf, yf, zf - 1), grad(p[ba + 1], xf - 1, yf, zf - 1)),
perlin_lerp(u, grad(p[ab + 1], xf, yf - 1, zf - 1), grad(p[bb + 1], xf - 1, yf - 1, zf - 1))));
}
static float perlin(float x, float y, float z)
{
LUAU_ASSERT(FFlag::LuauFasterNoise);
float xflr = floorf(x);
float yflr = floorf(y);
float zflr = floorf(z);
int xi = int(xflr) & 255;
int yi = int(yflr) & 255;
int zi = int(zflr) & 255;
float xf = x - xflr;
float yf = y - yflr;
float zf = z - zflr;
float u = perlin_fade(xf);
float v = perlin_fade(yf);
float w = perlin_fade(zf);
const unsigned char* p = kPerlinHash;
int a = (p[xi] + yi) & 255;
int aa = (p[a] + zi) & 255;
int ab = (p[a + 1] + zi) & 255;
int b = (p[xi + 1] + yi) & 255;
int ba = (p[b] + zi) & 255;
int bb = (p[b + 1] + zi) & 255;
float la = perlin_lerp(u, perlin_grad(p[aa], xf, yf, zf), perlin_grad(p[ba], xf - 1, yf, zf));
float lb = perlin_lerp(u, perlin_grad(p[ab], xf, yf - 1, zf), perlin_grad(p[bb], xf - 1, yf - 1, zf));
float la1 = perlin_lerp(u, perlin_grad(p[aa + 1], xf, yf, zf - 1), perlin_grad(p[ba + 1], xf - 1, yf, zf - 1));
float lb1 = perlin_lerp(u, perlin_grad(p[ab + 1], xf, yf - 1, zf - 1), perlin_grad(p[bb + 1], xf - 1, yf - 1, zf - 1));
return perlin_lerp(w, perlin_lerp(v, la, lb), perlin_lerp(v, la1, lb1));
}
static int math_noise(lua_State* L)
{
double x = luaL_checknumber(L, 1);
double y = luaL_optnumber(L, 2, 0.0);
double z = luaL_optnumber(L, 3, 0.0);
if (FFlag::LuauFasterNoise)
{
int nx, ny, nz;
double x = lua_tonumberx(L, 1, &nx);
double y = lua_tonumberx(L, 2, &ny);
double z = lua_tonumberx(L, 3, &nz);
double r = perlin((float)x, (float)y, (float)z);
luaL_argexpected(L, nx, 1, "number");
luaL_argexpected(L, ny || lua_isnoneornil(L, 2), 2, "number");
luaL_argexpected(L, nz || lua_isnoneornil(L, 3), 3, "number");
lua_pushnumber(L, r);
double r = perlin((float)x, (float)y, (float)z);
return 1;
lua_pushnumber(L, r);
return 1;
}
else
{
double x = luaL_checknumber(L, 1);
double y = luaL_optnumber(L, 2, 0.0);
double z = luaL_optnumber(L, 3, 0.0);
double r = perlin_dep((float)x, (float)y, (float)z);
lua_pushnumber(L, r);
return 1;
}
}
static int math_clamp(lua_State* L)

View file

@ -290,6 +290,8 @@ typedef struct Proto
uint8_t* typeinfo;
void* userdata;
GCObject* gclist;

View file

@ -10,6 +10,8 @@
#include "ldebug.h"
#include "lvm.h"
LUAU_FASTFLAGVARIABLE(LuauFasterTableConcat, false)
static int foreachi(lua_State* L)
{
luaL_checktype(L, 1, LUA_TTABLE);
@ -219,8 +221,8 @@ static int tmove(lua_State* L)
static void addfield(lua_State* L, luaL_Buffer* b, int i)
{
lua_rawgeti(L, 1, i);
if (!lua_isstring(L, -1))
int tt = lua_rawgeti(L, 1, i);
if (FFlag::LuauFasterTableConcat ? (tt != LUA_TSTRING && tt != LUA_TNUMBER) : !lua_isstring(L, -1))
luaL_error(L, "invalid value (%s) at index %d in table for 'concat'", luaL_typename(L, -1), i);
luaL_addvalue(b);
}

View file

@ -131,6 +131,9 @@
goto dispatchContinue
#endif
// Does VM support native execution via ExecutionCallbacks? We mostly assume it does but keep the define to make it easy to quantify the cost.
#define VM_HAS_NATIVE LUA_CUSTOM_EXECUTION
LUAU_NOINLINE void luau_callhook(lua_State* L, lua_Hook hook, void* userdata)
{
ptrdiff_t base = savestack(L, L->base);
@ -207,7 +210,7 @@ static void luau_execute(lua_State* L)
LUAU_ASSERT(L->isactive);
LUAU_ASSERT(!isblack(obj2gco(L))); // we don't use luaC_threadbarrier because active threads never turn black
#if LUA_CUSTOM_EXECUTION
#if VM_HAS_NATIVE
if ((L->ci->flags & LUA_CALLINFO_NATIVE) && !SingleStep)
{
Proto* p = clvalue(L->ci->func)->l.p;
@ -1036,7 +1039,7 @@ reentry:
Closure* nextcl = clvalue(cip->func);
Proto* nextproto = nextcl->l.p;
#if LUA_CUSTOM_EXECUTION
#if VM_HAS_NATIVE
if (LUAU_UNLIKELY((cip->flags & LUA_CALLINFO_NATIVE) && !SingleStep))
{
if (L->global->ecb.enter(L, nextproto) == 1)
@ -2371,7 +2374,7 @@ reentry:
ci->flags = LUA_CALLINFO_NATIVE;
ci->savedpc = p->code;
#if LUA_CUSTOM_EXECUTION
#if VM_HAS_NATIVE
if (L->global->ecb.enter(L, p) == 1)
goto reentry;
else
@ -2890,7 +2893,7 @@ int luau_precall(lua_State* L, StkId func, int nresults)
ci->savedpc = p->code;
#if LUA_CUSTOM_EXECUTION
#if VM_HAS_NATIVE
if (p->execdata)
ci->flags = LUA_CALLINFO_NATIVE;
#endif

View file

@ -0,0 +1,22 @@
local bench = script and require(script.Parent.bench_support) or require("bench_support")
bench.runCode(function()
for j=1,1e6 do
tonumber("42")
tonumber(42)
end
end, "tonumber")
bench.runCode(function()
for j=1,1e6 do
tostring(nil)
tostring("test")
tostring(42)
end
end, "tostring")
bench.runCode(function()
for j=1,1e6 do
tostring(j)
end
end, "tostring-gc")

View file

@ -15,6 +15,7 @@
LUAU_FASTFLAG(LuauTraceTypesInNonstrictMode2)
LUAU_FASTFLAG(LuauSetMetatableDoesNotTimeTravel)
LUAU_FASTFLAG(LuauAutocompleteLastTypecheck)
using namespace Luau;
@ -33,14 +34,40 @@ struct ACFixtureImpl : BaseType
AutocompleteResult autocomplete(unsigned row, unsigned column)
{
if (FFlag::LuauAutocompleteLastTypecheck)
{
FrontendOptions opts;
opts.forAutocomplete = true;
this->frontend.check("MainModule", opts);
}
return Luau::autocomplete(this->frontend, "MainModule", Position{row, column}, nullCallback);
}
AutocompleteResult autocomplete(char marker, StringCompletionCallback callback = nullCallback)
{
if (FFlag::LuauAutocompleteLastTypecheck)
{
FrontendOptions opts;
opts.forAutocomplete = true;
this->frontend.check("MainModule", opts);
}
return Luau::autocomplete(this->frontend, "MainModule", getPosition(marker), callback);
}
AutocompleteResult autocomplete(const ModuleName& name, Position pos, StringCompletionCallback callback = nullCallback)
{
if (FFlag::LuauAutocompleteLastTypecheck)
{
FrontendOptions opts;
opts.forAutocomplete = true;
this->frontend.check(name, opts);
}
return Luau::autocomplete(this->frontend, name, pos, callback);
}
CheckResult check(const std::string& source)
{
markerPosition.clear();
@ -99,7 +126,7 @@ struct ACFixtureImpl : BaseType
LUAU_ASSERT(i != markerPosition.end());
return i->second;
}
ScopedFastFlag flag{"LuauAutocompleteHideSelfArg", true};
// Maps a marker character (0-9 inclusive) to a position in the source code.
std::map<char, Position> markerPosition;
};
@ -1319,7 +1346,7 @@ local a: aa
frontend.check("Module/B");
auto ac = Luau::autocomplete(frontend, "Module/B", Position{2, 11}, nullCallback);
auto ac = autocomplete("Module/B", Position{2, 11});
CHECK(ac.entryMap.count("aaa"));
CHECK_EQ(ac.context, AutocompleteContext::Type);
@ -1342,7 +1369,7 @@ local a: aaa.
frontend.check("Module/B");
auto ac = Luau::autocomplete(frontend, "Module/B", Position{2, 13}, nullCallback);
auto ac = autocomplete("Module/B", Position{2, 13});
CHECK_EQ(2, ac.entryMap.size());
CHECK(ac.entryMap.count("A"));
@ -1999,7 +2026,7 @@ ex.a(function(x:
frontend.check("Module/B");
auto ac = Luau::autocomplete(frontend, "Module/B", Position{2, 16}, nullCallback);
auto ac = autocomplete("Module/B", Position{2, 16});
CHECK(!ac.entryMap.count("done"));
@ -2010,7 +2037,7 @@ ex.b(function(x:
frontend.check("Module/C");
ac = Luau::autocomplete(frontend, "Module/C", Position{2, 16}, nullCallback);
ac = autocomplete("Module/C", Position{2, 16});
CHECK(!ac.entryMap.count("(done) -> number"));
}
@ -2033,7 +2060,7 @@ ex.a(function(x:
frontend.check("Module/B");
auto ac = Luau::autocomplete(frontend, "Module/B", Position{2, 16}, nullCallback);
auto ac = autocomplete("Module/B", Position{2, 16});
CHECK(!ac.entryMap.count("done"));
CHECK(ac.entryMap.count("ex.done"));
@ -2046,7 +2073,7 @@ ex.b(function(x:
frontend.check("Module/C");
ac = Luau::autocomplete(frontend, "Module/C", Position{2, 16}, nullCallback);
ac = autocomplete("Module/C", Position{2, 16});
CHECK(!ac.entryMap.count("(done) -> number"));
CHECK(ac.entryMap.count("(ex.done) -> number"));
@ -2360,7 +2387,7 @@ local a: aaa.do
frontend.check("Module/B");
auto ac = Luau::autocomplete(frontend, "Module/B", Position{2, 15}, nullCallback);
auto ac = autocomplete("Module/B", Position{2, 15});
CHECK_EQ(2, ac.entryMap.size());
CHECK(ac.entryMap.count("done"));
@ -2372,7 +2399,7 @@ TEST_CASE_FIXTURE(ACFixture, "comments")
{
fileResolver.source["Comments"] = "--!str";
auto ac = Luau::autocomplete(frontend, "Comments", Position{0, 6}, nullCallback);
auto ac = autocomplete("Comments", Position{0, 6});
CHECK_EQ(0, ac.entryMap.size());
}
@ -2391,7 +2418,7 @@ TEST_CASE_FIXTURE(ACBuiltinsFixture, "autocompleteProp_index_function_metamethod
-- | Column 20
)";
auto ac = Luau::autocomplete(frontend, "Module/A", Position{9, 20}, nullCallback);
auto ac = autocomplete("Module/A", Position{9, 20});
REQUIRE_EQ(1, ac.entryMap.size());
CHECK(ac.entryMap.count("x"));
}
@ -2484,7 +2511,7 @@ TEST_CASE_FIXTURE(ACFixture, "not_the_var_we_are_defining")
{
fileResolver.source["Module/A"] = "abc,de";
auto ac = Luau::autocomplete(frontend, "Module/A", Position{0, 6}, nullCallback);
auto ac = autocomplete("Module/A", Position{0, 6});
CHECK(!ac.entryMap.count("de"));
}
@ -2495,7 +2522,7 @@ TEST_CASE_FIXTURE(ACFixture, "recursive_function_global")
end
)";
auto ac = Luau::autocomplete(frontend, "global", Position{1, 0}, nullCallback);
auto ac = autocomplete("global", Position{1, 0});
CHECK(ac.entryMap.count("abc"));
}
@ -2508,7 +2535,7 @@ TEST_CASE_FIXTURE(ACFixture, "recursive_function_local")
end
)";
auto ac = Luau::autocomplete(frontend, "local", Position{1, 0}, nullCallback);
auto ac = autocomplete("local", Position{1, 0});
CHECK(ac.entryMap.count("abc"));
}
@ -3147,6 +3174,8 @@ t:@1
REQUIRE(ac.entryMap.count("two"));
CHECK(!ac.entryMap["one"].wrongIndexType);
CHECK(ac.entryMap["two"].wrongIndexType);
CHECK(ac.entryMap["one"].indexedWithSelf);
CHECK(ac.entryMap["two"].indexedWithSelf);
}
{
@ -3161,6 +3190,8 @@ t.@1
REQUIRE(ac.entryMap.count("two"));
CHECK(ac.entryMap["one"].wrongIndexType);
CHECK(!ac.entryMap["two"].wrongIndexType);
CHECK(!ac.entryMap["one"].indexedWithSelf);
CHECK(!ac.entryMap["two"].indexedWithSelf);
}
}
@ -3190,6 +3221,7 @@ t:@1
REQUIRE(ac.entryMap.count("m"));
CHECK(!ac.entryMap["m"].wrongIndexType);
CHECK(ac.entryMap["m"].indexedWithSelf);
}
TEST_CASE_FIXTURE(ACFixture, "no_incompatible_self_calls")
@ -3204,6 +3236,7 @@ t:@1
REQUIRE(ac.entryMap.count("m"));
CHECK(ac.entryMap["m"].wrongIndexType);
CHECK(ac.entryMap["m"].indexedWithSelf);
}
TEST_CASE_FIXTURE(ACFixture, "no_incompatible_self_calls_2")
@ -3219,6 +3252,7 @@ t:@1
REQUIRE(ac.entryMap.count("f"));
CHECK(ac.entryMap["f"].wrongIndexType);
CHECK(ac.entryMap["f"].indexedWithSelf);
}
TEST_CASE_FIXTURE(ACFixture, "do_wrong_compatible_self_calls")
@ -3234,6 +3268,22 @@ t:@1
REQUIRE(ac.entryMap.count("m"));
// We can make changes to mark this as a wrong way to call even though it's compatible
CHECK(!ac.entryMap["m"].wrongIndexType);
CHECK(ac.entryMap["m"].indexedWithSelf);
}
TEST_CASE_FIXTURE(ACFixture, "do_wrong_compatible_nonself_calls")
{
check(R"(
local t = {}
function t:m(x: string) end
t.@1
)");
auto ac = autocomplete('1');
REQUIRE(ac.entryMap.count("m"));
CHECK(!ac.entryMap["m"].wrongIndexType);
CHECK(!ac.entryMap["m"].indexedWithSelf);
}
TEST_CASE_FIXTURE(ACFixture, "no_wrong_compatible_self_calls_with_generics")
@ -3249,6 +3299,7 @@ t:@1
REQUIRE(ac.entryMap.count("m"));
// While this call is compatible with the type, this requires instantiation of a generic type which we don't perform
CHECK(ac.entryMap["m"].wrongIndexType);
CHECK(ac.entryMap["m"].indexedWithSelf);
}
TEST_CASE_FIXTURE(ACFixture, "string_prim_self_calls_are_fine")
@ -3262,10 +3313,13 @@ s:@1
REQUIRE(ac.entryMap.count("byte"));
CHECK(ac.entryMap["byte"].wrongIndexType == false);
CHECK(ac.entryMap["byte"].indexedWithSelf);
REQUIRE(ac.entryMap.count("char"));
CHECK(ac.entryMap["char"].wrongIndexType == true);
CHECK(ac.entryMap["char"].indexedWithSelf);
REQUIRE(ac.entryMap.count("sub"));
CHECK(ac.entryMap["sub"].wrongIndexType == false);
CHECK(ac.entryMap["sub"].indexedWithSelf);
}
TEST_CASE_FIXTURE(ACFixture, "string_prim_non_self_calls_are_avoided")
@ -3279,8 +3333,10 @@ s.@1
REQUIRE(ac.entryMap.count("char"));
CHECK(ac.entryMap["char"].wrongIndexType == false);
CHECK(!ac.entryMap["char"].indexedWithSelf);
REQUIRE(ac.entryMap.count("sub"));
CHECK(ac.entryMap["sub"].wrongIndexType == true);
CHECK(!ac.entryMap["sub"].indexedWithSelf);
}
TEST_CASE_FIXTURE(ACBuiltinsFixture, "library_non_self_calls_are_fine")
@ -3293,10 +3349,13 @@ string.@1
REQUIRE(ac.entryMap.count("byte"));
CHECK(ac.entryMap["byte"].wrongIndexType == false);
CHECK(!ac.entryMap["byte"].indexedWithSelf);
REQUIRE(ac.entryMap.count("char"));
CHECK(ac.entryMap["char"].wrongIndexType == false);
CHECK(!ac.entryMap["char"].indexedWithSelf);
REQUIRE(ac.entryMap.count("sub"));
CHECK(ac.entryMap["sub"].wrongIndexType == false);
CHECK(!ac.entryMap["sub"].indexedWithSelf);
check(R"(
table.@1
@ -3306,10 +3365,13 @@ table.@1
REQUIRE(ac.entryMap.count("remove"));
CHECK(ac.entryMap["remove"].wrongIndexType == false);
CHECK(!ac.entryMap["remove"].indexedWithSelf);
REQUIRE(ac.entryMap.count("getn"));
CHECK(ac.entryMap["getn"].wrongIndexType == false);
CHECK(!ac.entryMap["getn"].indexedWithSelf);
REQUIRE(ac.entryMap.count("insert"));
CHECK(ac.entryMap["insert"].wrongIndexType == false);
CHECK(!ac.entryMap["insert"].indexedWithSelf);
}
TEST_CASE_FIXTURE(ACBuiltinsFixture, "library_self_calls_are_invalid")
@ -3322,13 +3384,16 @@ string:@1
REQUIRE(ac.entryMap.count("byte"));
CHECK(ac.entryMap["byte"].wrongIndexType == true);
CHECK(ac.entryMap["byte"].indexedWithSelf);
REQUIRE(ac.entryMap.count("char"));
CHECK(ac.entryMap["char"].wrongIndexType == true);
CHECK(ac.entryMap["char"].indexedWithSelf);
// We want the next test to evaluate to 'true', but we have to allow function defined with 'self' to be callable with ':'
// We may change the definition of the string metatable to not use 'self' types in the future (like byte/char/pack/unpack)
REQUIRE(ac.entryMap.count("sub"));
CHECK(ac.entryMap["sub"].wrongIndexType == false);
CHECK(ac.entryMap["sub"].indexedWithSelf);
}
TEST_CASE_FIXTURE(ACFixture, "source_module_preservation_and_invalidation")
@ -3489,4 +3554,480 @@ TEST_CASE_FIXTURE(ACFixture, "frontend_use_correct_global_scope")
CHECK(ac.entryMap.count("Name"));
}
TEST_CASE_FIXTURE(ACFixture, "string_completion_outside_quotes")
{
ScopedFastFlag flag{"LuauDisableCompletionOutsideQuotes", true};
loadDefinition(R"(
declare function require(path: string): any
)");
std::optional<Binding> require = frontend.globalsForAutocomplete.globalScope->linearSearchForBinding("require");
REQUIRE(require);
Luau::unfreeze(frontend.globalsForAutocomplete.globalTypes);
attachTag(require->typeId, "RequireCall");
Luau::freeze(frontend.globalsForAutocomplete.globalTypes);
check(R"(
local x = require(@1"@2"@3)
)");
StringCompletionCallback callback = [](std::string, std::optional<const ClassType*>,
std::optional<std::string> contents) -> std::optional<AutocompleteEntryMap>
{
Luau::AutocompleteEntryMap results = {{"test", Luau::AutocompleteEntry{Luau::AutocompleteEntryKind::String, std::nullopt, false, false}}};
return results;
};
auto ac = autocomplete('2', callback);
CHECK_EQ(1, ac.entryMap.size());
CHECK(ac.entryMap.count("test"));
ac = autocomplete('1', callback);
CHECK_EQ(0, ac.entryMap.size());
ac = autocomplete('3', callback);
CHECK_EQ(0, ac.entryMap.size());
}
TEST_CASE_FIXTURE(ACFixture, "anonymous_autofilled_empty")
{
ScopedFastFlag flag{"LuauAnonymousAutofilled", true};
check(R"(
local function foo(a: () -> ())
a()
end
foo(@1)
)");
const std::optional<std::string> EXPECTED_INSERT = "function() end";
auto ac = autocomplete('1');
REQUIRE(ac.entryMap.count(kGeneratedAnonymousFunctionEntryName) == 1);
CHECK(ac.entryMap[kGeneratedAnonymousFunctionEntryName].kind == Luau::AutocompleteEntryKind::GeneratedFunction);
CHECK(ac.entryMap[kGeneratedAnonymousFunctionEntryName].typeCorrect == Luau::TypeCorrectKind::Correct);
REQUIRE(ac.entryMap[kGeneratedAnonymousFunctionEntryName].insertText);
CHECK_EQ(EXPECTED_INSERT, *ac.entryMap[kGeneratedAnonymousFunctionEntryName].insertText);
}
TEST_CASE_FIXTURE(ACFixture, "anonymous_autofilled_args")
{
ScopedFastFlag flag{"LuauAnonymousAutofilled", true};
check(R"(
local function foo(a: (number, string) -> ())
a()
end
foo(@1)
)");
const std::optional<std::string> EXPECTED_INSERT = "function(a0: number, a1: string) end";
auto ac = autocomplete('1');
REQUIRE(ac.entryMap.count(kGeneratedAnonymousFunctionEntryName) == 1);
CHECK(ac.entryMap[kGeneratedAnonymousFunctionEntryName].kind == Luau::AutocompleteEntryKind::GeneratedFunction);
CHECK(ac.entryMap[kGeneratedAnonymousFunctionEntryName].typeCorrect == Luau::TypeCorrectKind::Correct);
REQUIRE(ac.entryMap[kGeneratedAnonymousFunctionEntryName].insertText);
CHECK_EQ(EXPECTED_INSERT, *ac.entryMap[kGeneratedAnonymousFunctionEntryName].insertText);
}
TEST_CASE_FIXTURE(ACFixture, "anonymous_autofilled_args_single_return")
{
ScopedFastFlag flag{"LuauAnonymousAutofilled", true};
check(R"(
local function foo(a: (number, string) -> (string))
a()
end
foo(@1)
)");
const std::optional<std::string> EXPECTED_INSERT = "function(a0: number, a1: string): string end";
auto ac = autocomplete('1');
REQUIRE(ac.entryMap.count(kGeneratedAnonymousFunctionEntryName) == 1);
CHECK(ac.entryMap[kGeneratedAnonymousFunctionEntryName].kind == Luau::AutocompleteEntryKind::GeneratedFunction);
CHECK(ac.entryMap[kGeneratedAnonymousFunctionEntryName].typeCorrect == Luau::TypeCorrectKind::Correct);
REQUIRE(ac.entryMap[kGeneratedAnonymousFunctionEntryName].insertText);
CHECK_EQ(EXPECTED_INSERT, *ac.entryMap[kGeneratedAnonymousFunctionEntryName].insertText);
}
TEST_CASE_FIXTURE(ACFixture, "anonymous_autofilled_args_multi_return")
{
ScopedFastFlag flag{"LuauAnonymousAutofilled", true};
check(R"(
local function foo(a: (number, string) -> (string, number))
a()
end
foo(@1)
)");
const std::optional<std::string> EXPECTED_INSERT = "function(a0: number, a1: string): (string, number) end";
auto ac = autocomplete('1');
REQUIRE(ac.entryMap.count(kGeneratedAnonymousFunctionEntryName) == 1);
CHECK(ac.entryMap[kGeneratedAnonymousFunctionEntryName].kind == Luau::AutocompleteEntryKind::GeneratedFunction);
CHECK(ac.entryMap[kGeneratedAnonymousFunctionEntryName].typeCorrect == Luau::TypeCorrectKind::Correct);
REQUIRE(ac.entryMap[kGeneratedAnonymousFunctionEntryName].insertText);
CHECK_EQ(EXPECTED_INSERT, *ac.entryMap[kGeneratedAnonymousFunctionEntryName].insertText);
}
TEST_CASE_FIXTURE(ACFixture, "anonymous_autofilled__noargs_multi_return")
{
ScopedFastFlag flag{"LuauAnonymousAutofilled", true};
check(R"(
local function foo(a: () -> (string, number))
a()
end
foo(@1)
)");
const std::optional<std::string> EXPECTED_INSERT = "function(): (string, number) end";
auto ac = autocomplete('1');
REQUIRE(ac.entryMap.count(kGeneratedAnonymousFunctionEntryName) == 1);
CHECK(ac.entryMap[kGeneratedAnonymousFunctionEntryName].kind == Luau::AutocompleteEntryKind::GeneratedFunction);
CHECK(ac.entryMap[kGeneratedAnonymousFunctionEntryName].typeCorrect == Luau::TypeCorrectKind::Correct);
REQUIRE(ac.entryMap[kGeneratedAnonymousFunctionEntryName].insertText);
CHECK_EQ(EXPECTED_INSERT, *ac.entryMap[kGeneratedAnonymousFunctionEntryName].insertText);
}
TEST_CASE_FIXTURE(ACFixture, "anonymous_autofilled__varargs_multi_return")
{
ScopedFastFlag flag{"LuauAnonymousAutofilled", true};
check(R"(
local function foo(a: (...number) -> (string, number))
a()
end
foo(@1)
)");
const std::optional<std::string> EXPECTED_INSERT = "function(...: number): (string, number) end";
auto ac = autocomplete('1');
REQUIRE(ac.entryMap.count(kGeneratedAnonymousFunctionEntryName) == 1);
CHECK(ac.entryMap[kGeneratedAnonymousFunctionEntryName].kind == Luau::AutocompleteEntryKind::GeneratedFunction);
CHECK(ac.entryMap[kGeneratedAnonymousFunctionEntryName].typeCorrect == Luau::TypeCorrectKind::Correct);
REQUIRE(ac.entryMap[kGeneratedAnonymousFunctionEntryName].insertText);
CHECK_EQ(EXPECTED_INSERT, *ac.entryMap[kGeneratedAnonymousFunctionEntryName].insertText);
}
TEST_CASE_FIXTURE(ACFixture, "anonymous_autofilled_multi_varargs_multi_return")
{
ScopedFastFlag flag{"LuauAnonymousAutofilled", true};
check(R"(
local function foo(a: (string, ...number) -> (string, number))
a()
end
foo(@1)
)");
const std::optional<std::string> EXPECTED_INSERT = "function(a0: string, ...: number): (string, number) end";
auto ac = autocomplete('1');
REQUIRE(ac.entryMap.count(kGeneratedAnonymousFunctionEntryName) == 1);
CHECK(ac.entryMap[kGeneratedAnonymousFunctionEntryName].kind == Luau::AutocompleteEntryKind::GeneratedFunction);
CHECK(ac.entryMap[kGeneratedAnonymousFunctionEntryName].typeCorrect == Luau::TypeCorrectKind::Correct);
REQUIRE(ac.entryMap[kGeneratedAnonymousFunctionEntryName].insertText);
CHECK_EQ(EXPECTED_INSERT, *ac.entryMap[kGeneratedAnonymousFunctionEntryName].insertText);
}
TEST_CASE_FIXTURE(ACFixture, "anonymous_autofilled_multi_varargs_varargs_return")
{
ScopedFastFlag flag{"LuauAnonymousAutofilled", true};
check(R"(
local function foo(a: (string, ...number) -> ...number)
a()
end
foo(@1)
)");
const std::optional<std::string> EXPECTED_INSERT = "function(a0: string, ...: number): ...number end";
auto ac = autocomplete('1');
REQUIRE(ac.entryMap.count(kGeneratedAnonymousFunctionEntryName) == 1);
CHECK(ac.entryMap[kGeneratedAnonymousFunctionEntryName].kind == Luau::AutocompleteEntryKind::GeneratedFunction);
CHECK(ac.entryMap[kGeneratedAnonymousFunctionEntryName].typeCorrect == Luau::TypeCorrectKind::Correct);
REQUIRE(ac.entryMap[kGeneratedAnonymousFunctionEntryName].insertText);
CHECK_EQ(EXPECTED_INSERT, *ac.entryMap[kGeneratedAnonymousFunctionEntryName].insertText);
}
TEST_CASE_FIXTURE(ACFixture, "anonymous_autofilled_multi_varargs_multi_varargs_return")
{
ScopedFastFlag flag{"LuauAnonymousAutofilled", true};
check(R"(
local function foo(a: (string, ...number) -> (boolean, ...number))
a()
end
foo(@1)
)");
const std::optional<std::string> EXPECTED_INSERT = "function(a0: string, ...: number): (boolean, ...number) end";
auto ac = autocomplete('1');
REQUIRE(ac.entryMap.count(kGeneratedAnonymousFunctionEntryName) == 1);
CHECK(ac.entryMap[kGeneratedAnonymousFunctionEntryName].kind == Luau::AutocompleteEntryKind::GeneratedFunction);
CHECK(ac.entryMap[kGeneratedAnonymousFunctionEntryName].typeCorrect == Luau::TypeCorrectKind::Correct);
REQUIRE(ac.entryMap[kGeneratedAnonymousFunctionEntryName].insertText);
CHECK_EQ(EXPECTED_INSERT, *ac.entryMap[kGeneratedAnonymousFunctionEntryName].insertText);
}
TEST_CASE_FIXTURE(ACFixture, "anonymous_autofilled_named_args")
{
ScopedFastFlag flag{"LuauAnonymousAutofilled", true};
check(R"(
local function foo(a: (foo: number, bar: string) -> (string, number))
a()
end
foo(@1)
)");
const std::optional<std::string> EXPECTED_INSERT = "function(foo: number, bar: string): (string, number) end";
auto ac = autocomplete('1');
REQUIRE(ac.entryMap.count(kGeneratedAnonymousFunctionEntryName) == 1);
CHECK(ac.entryMap[kGeneratedAnonymousFunctionEntryName].kind == Luau::AutocompleteEntryKind::GeneratedFunction);
CHECK(ac.entryMap[kGeneratedAnonymousFunctionEntryName].typeCorrect == Luau::TypeCorrectKind::Correct);
REQUIRE(ac.entryMap[kGeneratedAnonymousFunctionEntryName].insertText);
CHECK_EQ(EXPECTED_INSERT, *ac.entryMap[kGeneratedAnonymousFunctionEntryName].insertText);
}
TEST_CASE_FIXTURE(ACFixture, "anonymous_autofilled_partially_args")
{
ScopedFastFlag flag{"LuauAnonymousAutofilled", true};
check(R"(
local function foo(a: (number, bar: string) -> (string, number))
a()
end
foo(@1)
)");
const std::optional<std::string> EXPECTED_INSERT = "function(a0: number, bar: string): (string, number) end";
auto ac = autocomplete('1');
REQUIRE(ac.entryMap.count(kGeneratedAnonymousFunctionEntryName) == 1);
CHECK(ac.entryMap[kGeneratedAnonymousFunctionEntryName].kind == Luau::AutocompleteEntryKind::GeneratedFunction);
CHECK(ac.entryMap[kGeneratedAnonymousFunctionEntryName].typeCorrect == Luau::TypeCorrectKind::Correct);
REQUIRE(ac.entryMap[kGeneratedAnonymousFunctionEntryName].insertText);
CHECK_EQ(EXPECTED_INSERT, *ac.entryMap[kGeneratedAnonymousFunctionEntryName].insertText);
}
TEST_CASE_FIXTURE(ACFixture, "anonymous_autofilled_partially_args_last")
{
ScopedFastFlag flag{"LuauAnonymousAutofilled", true};
check(R"(
local function foo(a: (foo: number, string) -> (string, number))
a()
end
foo(@1)
)");
const std::optional<std::string> EXPECTED_INSERT = "function(foo: number, a1: string): (string, number) end";
auto ac = autocomplete('1');
REQUIRE(ac.entryMap.count(kGeneratedAnonymousFunctionEntryName) == 1);
CHECK(ac.entryMap[kGeneratedAnonymousFunctionEntryName].kind == Luau::AutocompleteEntryKind::GeneratedFunction);
CHECK(ac.entryMap[kGeneratedAnonymousFunctionEntryName].typeCorrect == Luau::TypeCorrectKind::Correct);
REQUIRE(ac.entryMap[kGeneratedAnonymousFunctionEntryName].insertText);
CHECK_EQ(EXPECTED_INSERT, *ac.entryMap[kGeneratedAnonymousFunctionEntryName].insertText);
}
TEST_CASE_FIXTURE(ACFixture, "anonymous_autofilled_typeof_args")
{
ScopedFastFlag flag{"LuauAnonymousAutofilled", true};
check(R"(
local t = { a = 1, b = 2 }
local function foo(a: (foo: typeof(t)) -> ())
a()
end
foo(@1)
)");
const std::optional<std::string> EXPECTED_INSERT = "function(foo) end"; // Cannot utter this type.
auto ac = autocomplete('1');
REQUIRE(ac.entryMap.count(kGeneratedAnonymousFunctionEntryName) == 1);
CHECK(ac.entryMap[kGeneratedAnonymousFunctionEntryName].kind == Luau::AutocompleteEntryKind::GeneratedFunction);
CHECK(ac.entryMap[kGeneratedAnonymousFunctionEntryName].typeCorrect == Luau::TypeCorrectKind::Correct);
REQUIRE(ac.entryMap[kGeneratedAnonymousFunctionEntryName].insertText);
CHECK_EQ(EXPECTED_INSERT, *ac.entryMap[kGeneratedAnonymousFunctionEntryName].insertText);
}
TEST_CASE_FIXTURE(ACFixture, "anonymous_autofilled_table_literal_args")
{
ScopedFastFlag flag{"LuauAnonymousAutofilled", true};
check(R"(
local function foo(a: (tbl: { x: number, y: number }) -> number) return a({x=2, y = 3}) end
foo(@1)
)");
const std::optional<std::string> EXPECTED_INSERT = "function(tbl: { x: number, y: number }): number end";
auto ac = autocomplete('1');
REQUIRE(ac.entryMap.count(kGeneratedAnonymousFunctionEntryName) == 1);
CHECK(ac.entryMap[kGeneratedAnonymousFunctionEntryName].kind == Luau::AutocompleteEntryKind::GeneratedFunction);
CHECK(ac.entryMap[kGeneratedAnonymousFunctionEntryName].typeCorrect == Luau::TypeCorrectKind::Correct);
REQUIRE(ac.entryMap[kGeneratedAnonymousFunctionEntryName].insertText);
CHECK_EQ(EXPECTED_INSERT, *ac.entryMap[kGeneratedAnonymousFunctionEntryName].insertText);
}
TEST_CASE_FIXTURE(ACFixture, "anonymous_autofilled_typeof_returns")
{
ScopedFastFlag flag{"LuauAnonymousAutofilled", true};
check(R"(
local t = { a = 1, b = 2 }
local function foo(a: () -> typeof(t))
a()
end
foo(@1)
)");
const std::optional<std::string> EXPECTED_INSERT = "function() end"; // Cannot utter this type.
auto ac = autocomplete('1');
REQUIRE(ac.entryMap.count(kGeneratedAnonymousFunctionEntryName) == 1);
CHECK(ac.entryMap[kGeneratedAnonymousFunctionEntryName].kind == Luau::AutocompleteEntryKind::GeneratedFunction);
CHECK(ac.entryMap[kGeneratedAnonymousFunctionEntryName].typeCorrect == Luau::TypeCorrectKind::Correct);
REQUIRE(ac.entryMap[kGeneratedAnonymousFunctionEntryName].insertText);
CHECK_EQ(EXPECTED_INSERT, *ac.entryMap[kGeneratedAnonymousFunctionEntryName].insertText);
}
TEST_CASE_FIXTURE(ACFixture, "anonymous_autofilled_table_literal_args")
{
ScopedFastFlag flag{"LuauAnonymousAutofilled", true};
check(R"(
local function foo(a: () -> { x: number, y: number }) return {x=2, y = 3} end
foo(@1)
)");
const std::optional<std::string> EXPECTED_INSERT = "function(): { x: number, y: number } end";
auto ac = autocomplete('1');
REQUIRE(ac.entryMap.count(kGeneratedAnonymousFunctionEntryName) == 1);
CHECK(ac.entryMap[kGeneratedAnonymousFunctionEntryName].kind == Luau::AutocompleteEntryKind::GeneratedFunction);
CHECK(ac.entryMap[kGeneratedAnonymousFunctionEntryName].typeCorrect == Luau::TypeCorrectKind::Correct);
REQUIRE(ac.entryMap[kGeneratedAnonymousFunctionEntryName].insertText);
CHECK_EQ(EXPECTED_INSERT, *ac.entryMap[kGeneratedAnonymousFunctionEntryName].insertText);
}
TEST_CASE_FIXTURE(ACFixture, "anonymous_autofilled_typeof_vararg")
{
ScopedFastFlag flag{"LuauAnonymousAutofilled", true};
check(R"(
local t = { a = 1, b = 2 }
local function foo(a: (...typeof(t)) -> ())
a()
end
foo(@1)
)");
const std::optional<std::string> EXPECTED_INSERT = "function(...) end"; // Cannot utter this type.
auto ac = autocomplete('1');
REQUIRE(ac.entryMap.count(kGeneratedAnonymousFunctionEntryName) == 1);
CHECK(ac.entryMap[kGeneratedAnonymousFunctionEntryName].kind == Luau::AutocompleteEntryKind::GeneratedFunction);
CHECK(ac.entryMap[kGeneratedAnonymousFunctionEntryName].typeCorrect == Luau::TypeCorrectKind::Correct);
REQUIRE(ac.entryMap[kGeneratedAnonymousFunctionEntryName].insertText);
CHECK_EQ(EXPECTED_INSERT, *ac.entryMap[kGeneratedAnonymousFunctionEntryName].insertText);
}
TEST_CASE_FIXTURE(ACFixture, "anonymous_autofilled_generic_type_pack_vararg")
{
ScopedFastFlag flag{"LuauAnonymousAutofilled", true};
check(R"(
local function foo<A>(a: (...A) -> number, ...: A)
return a(...)
end
foo(@1)
)");
const std::optional<std::string> EXPECTED_INSERT = "function(...): number end";
auto ac = autocomplete('1');
REQUIRE(ac.entryMap.count(kGeneratedAnonymousFunctionEntryName) == 1);
CHECK(ac.entryMap[kGeneratedAnonymousFunctionEntryName].kind == Luau::AutocompleteEntryKind::GeneratedFunction);
CHECK(ac.entryMap[kGeneratedAnonymousFunctionEntryName].typeCorrect == Luau::TypeCorrectKind::Correct);
REQUIRE(ac.entryMap[kGeneratedAnonymousFunctionEntryName].insertText);
CHECK_EQ(EXPECTED_INSERT, *ac.entryMap[kGeneratedAnonymousFunctionEntryName].insertText);
}
TEST_CASE_FIXTURE(ACFixture, "anonymous_autofilled_generic_on_argument_type_pack_vararg")
{
ScopedFastFlag flag{"LuauAnonymousAutofilled", true};
check(R"(
local function foo(a: <T...>(...: T...) -> number)
return a(4, 5, 6)
end
foo(@1)
)");
const std::optional<std::string> EXPECTED_INSERT = "function(...): number end";
auto ac = autocomplete('1');
REQUIRE(ac.entryMap.count(kGeneratedAnonymousFunctionEntryName) == 1);
CHECK(ac.entryMap[kGeneratedAnonymousFunctionEntryName].kind == Luau::AutocompleteEntryKind::GeneratedFunction);
CHECK(ac.entryMap[kGeneratedAnonymousFunctionEntryName].typeCorrect == Luau::TypeCorrectKind::Correct);
REQUIRE(ac.entryMap[kGeneratedAnonymousFunctionEntryName].insertText);
CHECK_EQ(EXPECTED_INSERT, *ac.entryMap[kGeneratedAnonymousFunctionEntryName].insertText);
}
TEST_SUITE_END();

View file

@ -6978,6 +6978,8 @@ L3: RETURN R0 0
TEST_CASE("BuiltinArity")
{
ScopedFastFlag sff("LuauCompileFixBuiltinArity", true);
// by default we can't assume that we know parameter/result count for builtins as they can be overridden at runtime
CHECK_EQ("\n" + compileFunction(R"(
return math.abs(unknown())
@ -7037,6 +7039,21 @@ FASTCALL 34 L0
GETIMPORT R0 4 [bit32.extract]
CALL R0 -1 1
L0: RETURN R0 1
)");
// some builtins are not variadic and have a fixed number of arguments but are not none-safe, meaning that we can't replace calls that may
// return none with calls that will return nil
CHECK_EQ("\n" + compileFunction(R"(
return type(unknown())
)",
0, 2),
R"(
GETIMPORT R1 1 [unknown]
CALL R1 0 -1
FASTCALL 40 L0
GETIMPORT R0 3 [type]
CALL R0 -1 1
L0: RETURN R0 1
)");
// importantly, this optimization also helps us get around the multret inlining restriction for builtin wrappers

View file

@ -273,6 +273,8 @@ TEST_CASE("Assert")
TEST_CASE("Basic")
{
ScopedFastFlag sff("LuauCompileFixBuiltinArity", true);
runConformance("basic.lua");
}
@ -326,6 +328,8 @@ TEST_CASE("Clear")
TEST_CASE("Strings")
{
ScopedFastFlag sff("LuauCompileFixBuiltinArity", true);
runConformance("strings.lua");
}
@ -1112,6 +1116,34 @@ static bool endsWith(const std::string& str, const std::string& suffix)
return suffix == std::string_view(str.c_str() + str.length() - suffix.length(), suffix.length());
}
TEST_CASE("ApiType")
{
StateRef globalState(luaL_newstate(), lua_close);
lua_State* L = globalState.get();
lua_pushnumber(L, 2);
CHECK(strcmp(luaL_typename(L, -1), "number") == 0);
CHECK(strcmp(luaL_typename(L, 1), "number") == 0);
CHECK(lua_type(L, -1) == LUA_TNUMBER);
CHECK(lua_type(L, 1) == LUA_TNUMBER);
CHECK(strcmp(luaL_typename(L, 2), "no value") == 0);
CHECK(lua_type(L, 2) == LUA_TNONE);
CHECK(strcmp(lua_typename(L, lua_type(L, 2)), "no value") == 0);
lua_newuserdata(L, 0);
CHECK(strcmp(luaL_typename(L, -1), "userdata") == 0);
CHECK(lua_type(L, -1) == LUA_TUSERDATA);
lua_newtable(L);
lua_pushstring(L, "hello");
lua_setfield(L, -2, "__type");
lua_setmetatable(L, -2);
CHECK(strcmp(luaL_typename(L, -1), "hello") == 0);
CHECK(lua_type(L, -1) == LUA_TUSERDATA);
}
#if !LUA_USE_LONGJMP
TEST_CASE("ExceptionObject")
{

View file

@ -21,7 +21,7 @@ void ConstraintGraphBuilderFixture::generateConstraints(const std::string& code)
AstStatBlock* root = parse(code);
dfg = std::make_unique<DataFlowGraph>(DataFlowGraphBuilder::build(root, NotNull{&ice}));
cgb = std::make_unique<ConstraintGraphBuilder>(mainModule, &arena, NotNull(&moduleResolver), builtinTypes, NotNull(&ice),
frontend.globals.globalScope, /*prepareModuleScope*/ nullptr, &logger, NotNull{dfg.get()});
frontend.globals.globalScope, /*prepareModuleScope*/ nullptr, &logger, NotNull{dfg.get()}, std::vector<RequireCycle>());
cgb->visit(root);
rootScope = cgb->rootScope;
constraints = Luau::borrowConstraints(cgb->constraints);
@ -30,7 +30,7 @@ void ConstraintGraphBuilderFixture::generateConstraints(const std::string& code)
void ConstraintGraphBuilderFixture::solve(const std::string& code)
{
generateConstraints(code);
ConstraintSolver cs{NotNull{&normalizer}, NotNull{rootScope}, constraints, "MainModule", NotNull(&moduleResolver), {}, &logger};
ConstraintSolver cs{NotNull{&normalizer}, NotNull{rootScope}, constraints, "MainModule", NotNull(&moduleResolver), {}, &logger, {}};
cs.run();
}

View file

@ -133,6 +133,8 @@ end
TEST_CASE("ControlFlow")
{
ScopedFastFlag sff("LuauAssignmentHasCost", true);
uint64_t model = modelFunction(R"(
function test(a)
while a < 0 do
@ -156,8 +158,8 @@ end
const bool args1[] = {false};
const bool args2[] = {true};
CHECK_EQ(82, Luau::Compile::computeCost(model, args1, 1));
CHECK_EQ(79, Luau::Compile::computeCost(model, args2, 1));
CHECK_EQ(76, Luau::Compile::computeCost(model, args1, 1));
CHECK_EQ(73, Luau::Compile::computeCost(model, args2, 1));
}
TEST_CASE("Conditional")
@ -240,4 +242,25 @@ end
CHECK_EQ(3, Luau::Compile::computeCost(model, args2, 1));
}
TEST_CASE("MultipleAssignments")
{
ScopedFastFlag sff("LuauAssignmentHasCost", true);
uint64_t model = modelFunction(R"(
function test(a)
local x = 0
x = a
x = a + 1
x, x, x = a
x = a, a, a
end
)");
const bool args1[] = {false};
const bool args2[] = {true};
CHECK_EQ(8, Luau::Compile::computeCost(model, args1, 1));
CHECK_EQ(7, Luau::Compile::computeCost(model, args2, 1));
}
TEST_SUITE_END();

File diff suppressed because it is too large Load diff

View file

@ -176,7 +176,7 @@ AstStatBlock* Fixture::parse(const std::string& source, const ParseOptions& pars
if (FFlag::DebugLuauDeferredConstraintResolution)
{
ModulePtr module = Luau::check(*sourceModule, {}, builtinTypes, NotNull{&ice}, NotNull{&moduleResolver}, NotNull{&fileResolver},
frontend.globals.globalScope, /*prepareModuleScope*/ nullptr, frontend.options);
frontend.globals.globalScope, /*prepareModuleScope*/ nullptr, frontend.options, {});
Luau::lint(sourceModule->root, *sourceModule->names, frontend.globals.globalScope, module.get(), sourceModule->hotcomments, {});
}
@ -415,6 +415,17 @@ TypeId Fixture::requireTypeAlias(const std::string& name)
return *ty;
}
TypeId Fixture::requireExportedType(const ModuleName& moduleName, const std::string& name)
{
ModulePtr module = frontend.moduleResolver.getModule(moduleName);
REQUIRE(module);
auto it = module->exportedTypeBindings.find(name);
REQUIRE(it != module->exportedTypeBindings.end());
return it->second.type;
}
std::string Fixture::decorateWithTypes(const std::string& code)
{
fileResolver.source[mainModuleName] = code;

View file

@ -2,6 +2,8 @@
#pragma once
#include "Luau/Config.h"
#include "Luau/Differ.h"
#include "Luau/Error.h"
#include "Luau/FileResolver.h"
#include "Luau/Frontend.h"
#include "Luau/IostreamHelpers.h"
@ -15,6 +17,7 @@
#include "IostreamOptional.h"
#include "ScopedFlags.h"
#include "doctest.h"
#include <string>
#include <unordered_map>
#include <optional>
@ -92,6 +95,7 @@ struct Fixture
std::optional<TypeId> lookupType(const std::string& name);
std::optional<TypeId> lookupImportedType(const std::string& moduleAlias, const std::string& name);
TypeId requireTypeAlias(const std::string& name);
TypeId requireExportedType(const ModuleName& moduleName, const std::string& name);
ScopedFastFlag sff_DebugLuauFreezeArena;
@ -153,6 +157,51 @@ std::optional<TypeId> linearSearchForBinding(Scope* scope, const char* name);
void registerHiddenTypes(Frontend* frontend);
void createSomeClasses(Frontend* frontend);
template<typename BaseFixture>
struct DifferFixtureGeneric : BaseFixture
{
void compareNe(TypeId left, TypeId right, const std::string& expectedMessage)
{
std::string diffMessage;
try
{
DifferResult diffRes = diff(left, right);
REQUIRE_MESSAGE(diffRes.diffError.has_value(), "Differ did not report type error, even though types are unequal");
diffMessage = diffRes.diffError->toString();
}
catch (const InternalCompilerError& e)
{
REQUIRE_MESSAGE(false, ("InternalCompilerError: " + e.message));
}
CHECK_EQ(expectedMessage, diffMessage);
}
void compareTypesNe(const std::string& leftSymbol, const std::string& rightSymbol, const std::string& expectedMessage)
{
compareNe(BaseFixture::requireType(leftSymbol), BaseFixture::requireType(rightSymbol), expectedMessage);
}
void compareEq(TypeId left, TypeId right)
{
try
{
DifferResult diffRes = diff(left, right);
CHECK_MESSAGE(!diffRes.diffError.has_value(), diffRes.diffError->toString());
}
catch (const InternalCompilerError& e)
{
REQUIRE_MESSAGE(false, ("InternalCompilerError: " + e.message));
}
}
void compareTypesEq(const std::string& leftSymbol, const std::string& rightSymbol)
{
compareEq(BaseFixture::requireType(leftSymbol), BaseFixture::requireType(rightSymbol));
}
};
using DifferFixture = DifferFixtureGeneric<Fixture>;
using DifferFixtureWithBuiltins = DifferFixtureGeneric<BuiltinsFixture>;
} // namespace Luau
#define LUAU_REQUIRE_ERRORS(result) \

View file

@ -444,6 +444,53 @@ TEST_CASE_FIXTURE(FrontendFixture, "cycle_incremental_type_surface_longer")
CHECK_EQ(toString(tyB), "any");
}
TEST_CASE_FIXTURE(FrontendFixture, "cycle_incremental_type_surface_exports")
{
ScopedFastFlag luauFixCyclicModuleExports{"LuauFixCyclicModuleExports", true};
fileResolver.source["game/A"] = R"(
local b = require(game.B)
export type atype = { x: b.btype }
return {mod_a = 1}
)";
fileResolver.source["game/B"] = R"(
export type btype = { x: number }
local function bf()
local a = require(game.A)
local bfl : a.atype = nil
return {bfl.x}
end
return {mod_b = 2}
)";
ToStringOptions opts;
opts.exhaustive = true;
CheckResult resultA = frontend.check("game/A");
LUAU_REQUIRE_ERRORS(resultA);
CheckResult resultB = frontend.check("game/B");
LUAU_REQUIRE_ERRORS(resultB);
TypeId tyB = requireExportedType("game/B", "btype");
CHECK_EQ(toString(tyB, opts), "{| x: number |}");
TypeId tyA = requireExportedType("game/A", "atype");
CHECK_EQ(toString(tyA, opts), "{| x: any |}");
frontend.markDirty("game/B");
resultB = frontend.check("game/B");
LUAU_REQUIRE_ERRORS(resultB);
tyB = requireExportedType("game/B", "btype");
CHECK_EQ(toString(tyB, opts), "{| x: number |}");
tyA = requireExportedType("game/A", "atype");
CHECK_EQ(toString(tyA, opts), "{| x: any |}");
}
TEST_CASE_FIXTURE(FrontendFixture, "dont_reparse_clean_file_when_linting")
{
fileResolver.source["Modules/A"] = R"(

View file

@ -1657,6 +1657,8 @@ _ = (math.random() < 0.5 and false) or 42 -- currently ignored
TEST_CASE_FIXTURE(Fixture, "WrongComment")
{
ScopedFastFlag sff("LuauLintNativeComment", true);
LintResult result = lint(R"(
--!strict
--!struct
@ -1666,17 +1668,19 @@ TEST_CASE_FIXTURE(Fixture, "WrongComment")
--!nolint UnknownGlobal
--! no more lint
--!strict here
--!native on
do end
--!nolint
)");
REQUIRE(6 == result.warnings.size());
REQUIRE(7 == result.warnings.size());
CHECK_EQ(result.warnings[0].text, "Unknown comment directive 'struct'; did you mean 'strict'?");
CHECK_EQ(result.warnings[1].text, "Unknown comment directive 'nolintGlobal'");
CHECK_EQ(result.warnings[2].text, "nolint directive refers to unknown lint rule 'Global'");
CHECK_EQ(result.warnings[3].text, "nolint directive refers to unknown lint rule 'KnownGlobal'; did you mean 'UnknownGlobal'?");
CHECK_EQ(result.warnings[4].text, "Comment directive with the type checking mode has extra symbols at the end of the line");
CHECK_EQ(result.warnings[5].text, "Comment directive is ignored because it is placed after the first non-comment token");
CHECK_EQ(result.warnings[5].text, "native directive has extra symbols at the end of the line");
CHECK_EQ(result.warnings[6].text, "Comment directive is ignored because it is placed after the first non-comment token");
}
TEST_CASE_FIXTURE(Fixture, "WrongCommentMuteSelf")

View file

@ -114,6 +114,17 @@ struct SimplifyFixture : Fixture
TEST_SUITE_BEGIN("Simplify");
TEST_CASE_FIXTURE(SimplifyFixture, "overload_negation_refinement_is_never")
{
TypeId f1 = mkFunction(stringTy, numberTy);
TypeId f2 = mkFunction(numberTy, stringTy);
TypeId intersection = arena->addType(IntersectionType{{f1, f2}});
TypeId unionT = arena->addType(UnionType{{errorTy, functionTy}});
TypeId negationT = mkNegation(unionT);
// The intersection of string -> number & number -> string, ~(error | function)
CHECK(neverTy == intersect(intersection, negationT));
}
TEST_CASE_FIXTURE(SimplifyFixture, "unknown_and_other_tops_and_bottom_types")
{
CHECK(unknownTy == intersect(unknownTy, unknownTy));

View file

@ -59,7 +59,7 @@ TEST_CASE("BenchmarkLevenshteinDistance")
auto end = std::chrono::steady_clock::now();
auto time = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
std::cout << "Running levenshtein distance " << count << " times took " << time.count() << "ms" << std::endl;
MESSAGE("Running levenshtein distance ", count, " times took ", time.count(), "ms");
}
#endif

View file

@ -11,6 +11,7 @@
using namespace Luau;
LUAU_FASTFLAG(LuauRecursiveTypeParameterRestriction);
LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution);
TEST_SUITE_BEGIN("ToString");

View file

@ -13,6 +13,8 @@
using namespace Luau;
LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution);
TEST_SUITE_BEGIN("TypeInferAnyError");
TEST_CASE_FIXTURE(Fixture, "for_in_loop_iterator_returns_any")

View file

@ -12,6 +12,8 @@
using namespace Luau;
using std::nullopt;
LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution);
TEST_SUITE_BEGIN("TypeInferClasses");
TEST_CASE_FIXTURE(ClassFixture, "call_method_of_a_class")

View file

@ -14,7 +14,8 @@
using namespace Luau;
LUAU_FASTFLAG(LuauInstantiateInSubtyping)
LUAU_FASTFLAG(LuauInstantiateInSubtyping);
LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution);
TEST_SUITE_BEGIN("TypeInferFunctions");
@ -2094,6 +2095,25 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "attempt_to_call_an_intersection_of_tables_wi
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "generic_packs_are_not_variadic")
{
ScopedFastFlag sff{"DebugLuauDeferredConstraintResolution", true};
CheckResult result = check(R"(
local function apply<a, b..., c...>(f: (a, b...) -> c..., x: a)
return f(x)
end
local function add(x: number, y: number)
return x + y
end
apply(add, 5)
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
}
TEST_CASE_FIXTURE(BuiltinsFixture, "num_is_solved_before_num_or_str")
{
CheckResult result = check(R"(

View file

@ -9,7 +9,8 @@
#include "doctest.h"
LUAU_FASTFLAG(LuauInstantiateInSubtyping)
LUAU_FASTFLAG(LuauInstantiateInSubtyping);
LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution);
using namespace Luau;

View file

@ -8,6 +8,7 @@
using namespace Luau;
LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution);
TEST_SUITE_BEGIN("IntersectionTypes");

View file

@ -9,6 +9,8 @@
using namespace Luau;
LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution);
TEST_SUITE_BEGIN("ProvisionalTests");
// These tests check for behavior that differs from the final behavior we'd
@ -502,6 +504,9 @@ TEST_CASE_FIXTURE(Fixture, "free_options_cannot_be_unified_together")
Normalizer normalizer{&arena, builtinTypes, NotNull{&sharedState}};
Unifier u{NotNull{&normalizer}, NotNull{scope.get()}, Location{}, Variance::Covariant};
if (FFlag::DebugLuauDeferredConstraintResolution)
u.enableNewSolver();
u.tryUnify(option1, option2);
CHECK(!u.failure);
@ -565,7 +570,7 @@ return wrapStrictTable(Constants, "Constants")
std::optional<TypeId> result = first(m->returnType);
REQUIRE(result);
if (FFlag::DebugLuauDeferredConstraintResolution)
CHECK_EQ("(any & ~table)?", toString(*result));
CHECK_EQ("(any & ~(*error-type* | table))?", toString(*result));
else
CHECK_MESSAGE(get<AnyType>(*result), *result);
}
@ -905,6 +910,9 @@ TEST_CASE_FIXTURE(Fixture, "free_options_can_be_unified_together")
Normalizer normalizer{&arena, builtinTypes, NotNull{&sharedState}};
Unifier u{NotNull{&normalizer}, NotNull{scope.get()}, Location{}, Variance::Covariant};
if (FFlag::DebugLuauDeferredConstraintResolution)
u.enableNewSolver();
u.tryUnify(option1, option2);
CHECK(!u.failure);

View file

@ -1820,4 +1820,46 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "refinements_should_preserve_error_suppressio
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(BuiltinsFixture, "many_refinements_on_val")
{
CheckResult result = check(R"(
local function is_nan(val: any): boolean
return type(val) == "number" and val ~= val
end
local function is_js_boolean(val: any): boolean
return not not val and val ~= 0 and val ~= "" and not is_nan(val)
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("(any) -> boolean", toString(requireType("is_nan")));
CHECK_EQ("(any) -> boolean", toString(requireType("is_js_boolean")));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "refine_unknown_to_table")
{
ScopedFastFlag sff{"DebugLuauDeferredConstraintResolution", true};
// this test is DCR-only as an instance of DCR fixing a bug in the old solver
CheckResult result = check(R"(
local a : unknown = nil
local idx, val
if typeof(a) == "table" then
for i, v in a do
idx = i
val = v
end
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("unknown", toString(requireType("idx")));
CHECK_EQ("unknown", toString(requireType("val")));
}
TEST_SUITE_END();

View file

@ -7,6 +7,8 @@
using namespace Luau;
LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution);
TEST_SUITE_BEGIN("TypeSingletons");
TEST_CASE_FIXTURE(Fixture, "function_args_infer_singletons")

View file

@ -132,6 +132,24 @@ TEST_CASE_FIXTURE(Fixture, "cannot_change_type_of_table_prop")
LUAU_REQUIRE_ERROR_COUNT(1, result);
}
TEST_CASE_FIXTURE(Fixture, "report_sensible_error_when_adding_a_value_to_a_nonexistent_prop")
{
CheckResult result = check(R"(
local t = {}
t.foo[1] = 'one'
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
INFO(result.errors[0]);
UnknownProperty* err = get<UnknownProperty>(result.errors[0]);
REQUIRE(err);
CHECK("t" == toString(err->table));
CHECK("foo" == err->key);
}
TEST_CASE_FIXTURE(Fixture, "function_calls_can_produce_tables")
{
CheckResult result = check("function get_table() return {prop=999} end get_table().prop = 0");
@ -439,8 +457,6 @@ TEST_CASE_FIXTURE(Fixture, "table_param_row_polymorphism_2")
)");
LUAU_REQUIRE_NO_ERRORS(result);
for (const auto& e : result.errors)
std::cout << "Error: " << e << std::endl;
TypeId qType = requireType("q");
const TableType* qTable = get<TableType>(qType);
@ -3642,4 +3658,75 @@ end
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "certain_properties_of_table_literal_arguments_can_be_covariant")
{
CheckResult result = check(R"(
function f(a: {[string]: string | {any} | nil })
return a
end
local x = f({
title = "Feature.VirtualEvents.EnableNotificationsModalTitle",
body = "Feature.VirtualEvents.EnableNotificationsModalBody",
notNow = "Feature.VirtualEvents.NotNowButton",
getNotified = "Feature.VirtualEvents.GetNotifiedButton",
})
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "subproperties_can_also_be_covariantly_tested")
{
CheckResult result = check(R"(
type T = {
[string]: {[string]: (string | number)?}
}
function f(t: T)
return t
end
local x = f({
subprop={x="hello"}
})
local y = f({
subprop={x=41}
})
local z = f({
subprop={}
})
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "cyclic_shifted_tables")
{
CheckResult result = check(R"(
local function id<a>(x: a): a
return x
end
-- Remove name from cyclic table
local foo = id({})
foo.foo = id({})
foo.foo.foo = id({})
foo.foo.foo.foo = id({})
foo.foo.foo.foo.foo = foo
local almostFoo = id({})
almostFoo.foo = id({})
almostFoo.foo.foo = id({})
almostFoo.foo.foo.foo = id({})
almostFoo.foo.foo.foo.foo = almostFoo
-- Shift
almostFoo = almostFoo.foo.foo
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_SUITE_END();

View file

@ -224,7 +224,7 @@ TEST_CASE_FIXTURE(Fixture, "crazy_complexity")
A:A():A():A():A():A():A():A():A():A():A():A()
)");
std::cout << "OK! Allocated " << typeChecker.types.size() << " types" << std::endl;
MESSAGE("OK! Allocated ", typeChecker.types.size(), " types");
}
#endif
@ -1332,4 +1332,43 @@ TEST_CASE_FIXTURE(Fixture, "handle_self_referential_HasProp_constraints")
)");
}
/* We had an issue where we were unifying two type packs
*
* free-2-0... and (string, free-4-0...)
*
* The correct thing to do here is to promote everything on the right side to
* level 2-0 before binding the left pack to the right. If we fail to do this,
* then the code fragment here fails to typecheck because the argument and
* return types of C are generalized before we ever get to checking the body of
* C.
*/
TEST_CASE_FIXTURE(Fixture, "promote_tail_type_packs")
{
CheckResult result = check(R"(
--!strict
local A: any = nil
local C
local D = A(
A({}, {
__call = function(a): string
local E: string = C(a)
return E
end
}),
{
F = function(s: typeof(C))
end
}
)
function C(b: any): string
return ''
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_SUITE_END();

View file

@ -345,7 +345,6 @@ TEST_CASE_FIXTURE(TryUnifyFixture, "metatables_unify_against_shape_of_free_table
{
ScopedFastFlag sff[] = {
{"LuauTransitiveSubtyping", true},
{"DebugLuauDeferredConstraintResolution", true},
};
TableType::Props freeProps{
@ -369,6 +368,7 @@ TEST_CASE_FIXTURE(TryUnifyFixture, "metatables_unify_against_shape_of_free_table
TypeId target = arena.addType(TableType{TableState::Unsealed, TypeLevel{}});
TypeId metatable = arena.addType(MetatableType{target, mt});
state.enableNewSolver();
state.tryUnify(metatable, free);
state.log.commit();
@ -439,11 +439,10 @@ TEST_CASE_FIXTURE(TryUnifyFixture, "unifying_two_unions_under_dcr_does_not_creat
const TypeId innerType = arena.freshType(nestedScope.get());
ScopedFastFlag sffs[]{
{"DebugLuauDeferredConstraintResolution", true},
{"LuauAlwaysCommitInferencesOfFunctionCalls", true},
};
state.enableScopeTests();
state.enableNewSolver();
SUBCASE("equal_scopes")
{

View file

@ -9,6 +9,8 @@
using namespace Luau;
LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution);
TEST_SUITE_BEGIN("TypePackTests");
TEST_CASE_FIXTURE(Fixture, "infer_multi_return")

View file

@ -8,6 +8,8 @@
using namespace Luau;
LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution);
TEST_SUITE_BEGIN("UnionTypes");
TEST_CASE_FIXTURE(Fixture, "return_types_can_be_disjoint")

View file

@ -6,6 +6,8 @@
using namespace Luau;
LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution);
TEST_SUITE_BEGIN("TypeInferUnknownNever");
TEST_CASE_FIXTURE(Fixture, "string_subtype_and_unknown_supertype")

View file

@ -944,6 +944,11 @@ end)(true) == 5050)
assert(pcall(typeof) == false)
assert(pcall(type) == false)
function nothing() end
assert(pcall(function() return typeof(nothing()) end) == false)
assert(pcall(function() return type(nothing()) end) == false)
-- typeof == type in absence of custom userdata
assert(concat(typeof(5), typeof(nil), typeof({}), typeof(newproxy())) == "number,nil,table,userdata")

View file

@ -235,6 +235,12 @@ assert(flag);
assert(select(2, pcall(math.random, 1, 2, 3)):match("wrong number of arguments"))
-- argument count
function nothing() end
assert(pcall(math.abs) == false)
assert(pcall(function() return math.abs(nothing()) end) == false)
-- min/max
assert(math.min(1) == 1)
assert(math.min(1, 2) == 1)
@ -249,6 +255,7 @@ assert(math.max(1, -1, 2) == 2)
assert(math.noise(0.5) == 0)
assert(math.noise(0.5, 0.5) == -0.25)
assert(math.noise(0.5, 0.5, -0.5) == 0.125)
assert(math.noise(455.7204209769105, 340.80410508750134, 121.80087666537628) == 0.5010709762573242)
local inf = math.huge * 2
local nan = 0 / 0

View file

@ -107,6 +107,12 @@ assert(tostring(1234567890123) == '1234567890123')
assert(#tostring('\0') == 1)
assert(tostring(true) == "true")
assert(tostring(false) == "false")
function nothing() end
assert(pcall(tostring) == false)
assert(pcall(function() return tostring(nothing()) end) == false)
print('+')
x = '"ílo"\n\\'

View file

@ -201,7 +201,7 @@ static FValueResult<bool> parseFFlag(std::string_view view)
auto [name, value] = parseFValueHelper(view);
bool state = value ? *value == "true" : true;
if (value && value != "true" && value != "false")
std::cerr << "Ignored '" << name << "' because '" << *value << "' is not a valid FFlag state." << std::endl;
fprintf(stderr, "Ignored '%s' because '%s' is not a valid flag state\n", name.c_str(), value->c_str());
return {name, state};
}
@ -264,9 +264,7 @@ int main(int argc, char** argv)
if (skipFastFlag(flag->name))
continue;
if (flag->dynamic)
std::cout << 'D';
std::cout << "FFlag" << flag->name << std::endl;
printf("%sFFlag%s\n", flag->dynamic ? "D" : "", flag->name);
}
return 0;
@ -286,7 +284,7 @@ int main(int argc, char** argv)
if (doctest::parseIntOption(argc, argv, "-O", doctest::option_int, level))
{
if (level < 0 || level > 2)
std::cerr << "Optimization level must be between 0 and 2 inclusive." << std::endl;
fprintf(stderr, "Optimization level must be between 0 and 2 inclusive\n");
else
optimizationLevel = level;
}

View file

@ -1,4 +1,6 @@
AstQuery.last_argument_function_call_type
AutocompleteTest.anonymous_autofilled_generic_on_argument_type_pack_vararg
AutocompleteTest.anonymous_autofilled_generic_type_pack_vararg
BuiltinTests.aliased_string_format
BuiltinTests.assert_removes_falsy_types
BuiltinTests.assert_removes_falsy_types2
@ -55,7 +57,6 @@ ProvisionalTests.typeguard_inference_incomplete
RefinementTest.discriminate_from_truthiness_of_x
RefinementTest.not_t_or_some_prop_of_t
RefinementTest.refine_a_property_of_some_global
RefinementTest.refinements_should_preserve_error_suppression
RefinementTest.truthy_constraint_on_properties
RefinementTest.type_narrow_to_vector
RefinementTest.typeguard_cast_free_table_to_vector
@ -69,9 +70,6 @@ TableTests.dont_crash_when_setmetatable_does_not_produce_a_metatabletypevar
TableTests.dont_hang_when_trying_to_look_up_in_cyclic_metatable_index
TableTests.dont_suggest_exact_match_keys
TableTests.error_detailed_metatable_prop
TableTests.expected_indexer_from_table_union
TableTests.expected_indexer_value_type_extra
TableTests.expected_indexer_value_type_extra_2
TableTests.explicitly_typed_table
TableTests.explicitly_typed_table_with_indexer
TableTests.fuzz_table_unify_instantiated_table
@ -128,6 +126,7 @@ TypeInfer.follow_on_new_types_in_substitution
TypeInfer.fuzz_free_table_type_change_during_index_check
TypeInfer.infer_assignment_value_types_mutable_lval
TypeInfer.no_stack_overflow_from_isoptional
TypeInfer.recursive_function_that_invokes_itself_with_a_refinement_of_its_parameter_2
TypeInfer.tc_after_error_recovery_no_replacement_name_in_error
TypeInfer.type_infer_recursion_limit_no_ice
TypeInfer.type_infer_recursion_limit_normalizer