Sync to upstream/release/594 (#1036)

* Fixed `Frontend::markDirty` not working on modules that were not
typechecked yet
* Fixed generic variadic function unification succeeding when it should
have reported an error

New Type Solver:
* Implemented semantic subtyping check for function types

Native Code Generation:
* Improved performance of numerical loops with a constant step
* Simplified IR for `bit32.extract` calls extracting first/last bits
* Improved performance of NaN checks
This commit is contained in:
vegorov-rbx 2023-09-07 17:13:49 -07:00 committed by GitHub
parent bf1fb8f1e4
commit c7c986b996
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
46 changed files with 1628 additions and 1070 deletions

View File

@ -14,8 +14,6 @@ struct GlobalTypes;
struct TypeChecker;
struct TypeArena;
void registerBuiltinTypes(GlobalTypes& globals);
void registerBuiltinGlobals(Frontend& frontend, GlobalTypes& globals, bool typeCheckForAutocomplete = false);
TypeId makeUnion(TypeArena& arena, std::vector<TypeId>&& types);
TypeId makeIntersection(TypeArena& arena, std::vector<TypeId>&& types);

View File

@ -2,12 +2,12 @@
#pragma once
#include "Luau/Config.h"
#include "Luau/GlobalTypes.h"
#include "Luau/Module.h"
#include "Luau/ModuleResolver.h"
#include "Luau/RequireTracer.h"
#include "Luau/Scope.h"
#include "Luau/TypeCheckLimits.h"
#include "Luau/TypeInfer.h"
#include "Luau/Variant.h"
#include <mutex>

View File

@ -0,0 +1,26 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#pragma once
#include "Luau/Module.h"
#include "Luau/NotNull.h"
#include "Luau/Scope.h"
#include "Luau/TypeArena.h"
namespace Luau
{
struct BuiltinTypes;
struct GlobalTypes
{
explicit GlobalTypes(NotNull<BuiltinTypes> builtinTypes);
NotNull<BuiltinTypes> builtinTypes; // Global types are based on builtin types
TypeArena globalTypes;
SourceModule globalNames; // names for symbols entered into globalScope
ScopePtr globalScope; // shared by all modules
};
}

View File

@ -19,6 +19,7 @@ class TypeIds;
class Normalizer;
struct NormalizedType;
struct NormalizedClassType;
struct NormalizedFunctionType;
struct SubtypingResult
{
@ -103,6 +104,7 @@ private:
SubtypingResult isSubtype_(const NormalizedType* subNorm, const NormalizedType* superNorm);
SubtypingResult isSubtype_(const NormalizedClassType& subClass, const NormalizedClassType& superClass, const TypeIds& superTables);
SubtypingResult isSubtype_(const NormalizedFunctionType& subFunction, const NormalizedFunctionType& superFunction);
SubtypingResult isSubtype_(const TypeIds& subTypes, const TypeIds& superTypes);
SubtypingResult isSubtype_(const VariadicTypePack* subVariadic, const VariadicTypePack* superVariadic);

View File

@ -798,12 +798,13 @@ struct BuiltinTypes
TypeId errorRecoveryType() const;
TypePackId errorRecoveryTypePack() const;
friend TypeId makeStringMetatable(NotNull<BuiltinTypes> builtinTypes);
friend struct GlobalTypes;
private:
std::unique_ptr<struct TypeArena> arena;
bool debugFreezeArena = false;
TypeId makeStringMetatable();
public:
const TypeId nilType;
const TypeId numberType;

View File

@ -57,17 +57,6 @@ struct HashBoolNamePair
size_t operator()(const std::pair<bool, Name>& pair) const;
};
struct GlobalTypes
{
GlobalTypes(NotNull<BuiltinTypes> builtinTypes);
NotNull<BuiltinTypes> builtinTypes; // Global types are based on builtin types
TypeArena globalTypes;
SourceModule globalNames; // names for symbols entered into globalScope
ScopePtr globalScope; // shared by all modules
};
// All Types are retained via Environment::types. All TypeIds
// within a program are borrowed pointers into this set.
struct TypeChecker

View File

@ -13,8 +13,6 @@
#include <utility>
LUAU_FASTFLAG(DebugLuauReadWriteProperties)
LUAU_FASTFLAGVARIABLE(LuauAnonymousAutofilled1, false);
LUAU_FASTFLAGVARIABLE(LuauAutocompleteLastTypecheck, false)
LUAU_FASTFLAGVARIABLE(LuauAutocompleteDoEnd, false)
LUAU_FASTFLAGVARIABLE(LuauAutocompleteStringLiteralBounds, false);
@ -611,7 +609,6 @@ std::optional<TypeId> getLocalTypeInScopeAt(const Module& module, Position posit
template <typename T>
static std::optional<std::string> tryToStringDetailed(const ScopePtr& scope, T ty, bool functionTypeArguments)
{
LUAU_ASSERT(FFlag::LuauAnonymousAutofilled1);
ToStringOptions opts;
opts.useLineBreaks = false;
opts.hideTableKind = true;
@ -630,23 +627,7 @@ static std::optional<Name> tryGetTypeNameInScope(ScopePtr scope, TypeId ty, bool
if (!canSuggestInferredType(scope, ty))
return std::nullopt;
if (FFlag::LuauAnonymousAutofilled1)
{
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;
}
return tryToStringDetailed(scope, ty, functionTypeArguments);
}
static bool tryAddTypeCorrectSuggestion(AutocompleteEntryMap& result, ScopePtr scope, AstType* topType, TypeId inferredType, Position position)
@ -1417,7 +1398,6 @@ static AutocompleteResult autocompleteWhileLoopKeywords(std::vector<AstNode*> an
static std::string makeAnonymous(const ScopePtr& scope, const FunctionType& funcTy)
{
LUAU_ASSERT(FFlag::LuauAnonymousAutofilled1);
std::string result = "function(";
auto [args, tail] = Luau::flatten(funcTy.argTypes);
@ -1483,7 +1463,6 @@ static std::string makeAnonymous(const ScopePtr& scope, const FunctionType& func
static std::optional<AutocompleteEntry> makeAnonymousAutofilled(const ModulePtr& module, Position position, const AstNode* node, const std::vector<AstNode*>& ancestry)
{
LUAU_ASSERT(FFlag::LuauAnonymousAutofilled1);
const AstExprCall* call = node->as<AstExprCall>();
if (!call && ancestry.size() > 1)
call = ancestry[ancestry.size() - 2]->as<AstExprCall>();
@ -1801,17 +1780,10 @@ static AutocompleteResult autocomplete(const SourceModule& sourceModule, const M
if (node->asExpr())
{
if (FFlag::LuauAnonymousAutofilled1)
{
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);
}
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 if (node->asStat())
return {autocompleteStatement(sourceModule, *module, ancestry, position), ancestry, AutocompleteContext::Statement};
@ -1821,15 +1793,6 @@ static AutocompleteResult autocomplete(const SourceModule& sourceModule, const M
AutocompleteResult autocomplete(Frontend& frontend, const ModuleName& moduleName, Position position, StringCompletionCallback callback)
{
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)
return {};

View File

@ -201,18 +201,6 @@ void assignPropDocumentationSymbols(TableType::Props& props, const std::string&
}
}
void registerBuiltinTypes(GlobalTypes& globals)
{
globals.globalScope->addBuiltinTypeBinding("any", TypeFun{{}, globals.builtinTypes->anyType});
globals.globalScope->addBuiltinTypeBinding("nil", TypeFun{{}, globals.builtinTypes->nilType});
globals.globalScope->addBuiltinTypeBinding("number", TypeFun{{}, globals.builtinTypes->numberType});
globals.globalScope->addBuiltinTypeBinding("string", TypeFun{{}, globals.builtinTypes->stringType});
globals.globalScope->addBuiltinTypeBinding("boolean", TypeFun{{}, globals.builtinTypes->booleanType});
globals.globalScope->addBuiltinTypeBinding("thread", TypeFun{{}, globals.builtinTypes->threadType});
globals.globalScope->addBuiltinTypeBinding("unknown", TypeFun{{}, globals.builtinTypes->unknownType});
globals.globalScope->addBuiltinTypeBinding("never", TypeFun{{}, globals.builtinTypes->neverType});
}
void registerBuiltinGlobals(Frontend& frontend, GlobalTypes& globals, bool typeCheckForAutocomplete)
{
LUAU_ASSERT(!globals.globalTypes.types.isFrozen());
@ -310,6 +298,520 @@ void registerBuiltinGlobals(Frontend& frontend, GlobalTypes& globals, bool typeC
attachDcrMagicFunction(getGlobalBinding(globals, "require"), dcrMagicFunctionRequire);
}
static std::vector<TypeId> parseFormatString(NotNull<BuiltinTypes> builtinTypes, const char* data, size_t size)
{
const char* options = "cdiouxXeEfgGqs*";
std::vector<TypeId> result;
for (size_t i = 0; i < size; ++i)
{
if (data[i] == '%')
{
i++;
if (i < size && data[i] == '%')
continue;
// we just ignore all characters (including flags/precision) up until first alphabetic character
while (i < size && !(data[i] > 0 && (isalpha(data[i]) || data[i] == '*')))
i++;
if (i == size)
break;
if (data[i] == 'q' || data[i] == 's')
result.push_back(builtinTypes->stringType);
else if (data[i] == '*')
result.push_back(builtinTypes->unknownType);
else if (strchr(options, data[i]))
result.push_back(builtinTypes->numberType);
else
result.push_back(builtinTypes->errorRecoveryType(builtinTypes->anyType));
}
}
return result;
}
std::optional<WithPredicate<TypePackId>> magicFunctionFormat(
TypeChecker& typechecker, const ScopePtr& scope, const AstExprCall& expr, WithPredicate<TypePackId> withPredicate)
{
auto [paramPack, _predicates] = withPredicate;
TypeArena& arena = typechecker.currentModule->internalTypes;
AstExprConstantString* fmt = nullptr;
if (auto index = expr.func->as<AstExprIndexName>(); index && expr.self)
{
if (auto group = index->expr->as<AstExprGroup>())
fmt = group->expr->as<AstExprConstantString>();
else
fmt = index->expr->as<AstExprConstantString>();
}
if (!expr.self && expr.args.size > 0)
fmt = expr.args.data[0]->as<AstExprConstantString>();
if (!fmt)
return std::nullopt;
std::vector<TypeId> expected = parseFormatString(typechecker.builtinTypes, fmt->value.data, fmt->value.size);
const auto& [params, tail] = flatten(paramPack);
size_t paramOffset = 1;
size_t dataOffset = expr.self ? 0 : 1;
// unify the prefix one argument at a time
for (size_t i = 0; i < expected.size() && i + paramOffset < params.size(); ++i)
{
Location location = expr.args.data[std::min(i + dataOffset, expr.args.size - 1)]->location;
typechecker.unify(params[i + paramOffset], expected[i], scope, location);
}
// if we know the argument count or if we have too many arguments for sure, we can issue an error
size_t numActualParams = params.size();
size_t numExpectedParams = expected.size() + 1; // + 1 for the format string
if (numExpectedParams != numActualParams && (!tail || numExpectedParams < numActualParams))
typechecker.reportError(TypeError{expr.location, CountMismatch{numExpectedParams, std::nullopt, numActualParams}});
return WithPredicate<TypePackId>{arena.addTypePack({typechecker.stringType})};
}
static bool dcrMagicFunctionFormat(MagicFunctionCallContext context)
{
TypeArena* arena = context.solver->arena;
AstExprConstantString* fmt = nullptr;
if (auto index = context.callSite->func->as<AstExprIndexName>(); index && context.callSite->self)
{
if (auto group = index->expr->as<AstExprGroup>())
fmt = group->expr->as<AstExprConstantString>();
else
fmt = index->expr->as<AstExprConstantString>();
}
if (!context.callSite->self && context.callSite->args.size > 0)
fmt = context.callSite->args.data[0]->as<AstExprConstantString>();
if (!fmt)
return false;
std::vector<TypeId> expected = parseFormatString(context.solver->builtinTypes, fmt->value.data, fmt->value.size);
const auto& [params, tail] = flatten(context.arguments);
size_t paramOffset = 1;
// unify the prefix one argument at a time
for (size_t i = 0; i < expected.size() && i + paramOffset < params.size(); ++i)
{
context.solver->unify(context.solver->rootScope, context.callSite->location, params[i + paramOffset], expected[i]);
}
// if we know the argument count or if we have too many arguments for sure, we can issue an error
size_t numActualParams = params.size();
size_t numExpectedParams = expected.size() + 1; // + 1 for the format string
if (numExpectedParams != numActualParams && (!tail || numExpectedParams < numActualParams))
context.solver->reportError(TypeError{context.callSite->location, CountMismatch{numExpectedParams, std::nullopt, numActualParams}});
TypePackId resultPack = arena->addTypePack({context.solver->builtinTypes->stringType});
asMutable(context.result)->ty.emplace<BoundTypePack>(resultPack);
return true;
}
static std::vector<TypeId> parsePatternString(NotNull<BuiltinTypes> builtinTypes, const char* data, size_t size)
{
std::vector<TypeId> result;
int depth = 0;
bool parsingSet = false;
for (size_t i = 0; i < size; ++i)
{
if (data[i] == '%')
{
++i;
if (!parsingSet && i < size && data[i] == 'b')
i += 2;
}
else if (!parsingSet && data[i] == '[')
{
parsingSet = true;
if (i + 1 < size && data[i + 1] == ']')
i += 1;
}
else if (parsingSet && data[i] == ']')
{
parsingSet = false;
}
else if (data[i] == '(')
{
if (parsingSet)
continue;
if (i + 1 < size && data[i + 1] == ')')
{
i++;
result.push_back(builtinTypes->optionalNumberType);
continue;
}
++depth;
result.push_back(builtinTypes->optionalStringType);
}
else if (data[i] == ')')
{
if (parsingSet)
continue;
--depth;
if (depth < 0)
break;
}
}
if (depth != 0 || parsingSet)
return std::vector<TypeId>();
if (result.empty())
result.push_back(builtinTypes->optionalStringType);
return result;
}
static std::optional<WithPredicate<TypePackId>> magicFunctionGmatch(
TypeChecker& typechecker, const ScopePtr& scope, const AstExprCall& expr, WithPredicate<TypePackId> withPredicate)
{
auto [paramPack, _predicates] = withPredicate;
const auto& [params, tail] = flatten(paramPack);
if (params.size() != 2)
return std::nullopt;
TypeArena& arena = typechecker.currentModule->internalTypes;
AstExprConstantString* pattern = nullptr;
size_t index = expr.self ? 0 : 1;
if (expr.args.size > index)
pattern = expr.args.data[index]->as<AstExprConstantString>();
if (!pattern)
return std::nullopt;
std::vector<TypeId> returnTypes = parsePatternString(typechecker.builtinTypes, pattern->value.data, pattern->value.size);
if (returnTypes.empty())
return std::nullopt;
typechecker.unify(params[0], typechecker.stringType, scope, expr.args.data[0]->location);
const TypePackId emptyPack = arena.addTypePack({});
const TypePackId returnList = arena.addTypePack(returnTypes);
const TypeId iteratorType = arena.addType(FunctionType{emptyPack, returnList});
return WithPredicate<TypePackId>{arena.addTypePack({iteratorType})};
}
static bool dcrMagicFunctionGmatch(MagicFunctionCallContext context)
{
const auto& [params, tail] = flatten(context.arguments);
if (params.size() != 2)
return false;
TypeArena* arena = context.solver->arena;
AstExprConstantString* pattern = nullptr;
size_t index = context.callSite->self ? 0 : 1;
if (context.callSite->args.size > index)
pattern = context.callSite->args.data[index]->as<AstExprConstantString>();
if (!pattern)
return false;
std::vector<TypeId> returnTypes = parsePatternString(context.solver->builtinTypes, pattern->value.data, pattern->value.size);
if (returnTypes.empty())
return false;
context.solver->unify(context.solver->rootScope, context.callSite->location, params[0], context.solver->builtinTypes->stringType);
const TypePackId emptyPack = arena->addTypePack({});
const TypePackId returnList = arena->addTypePack(returnTypes);
const TypeId iteratorType = arena->addType(FunctionType{emptyPack, returnList});
const TypePackId resTypePack = arena->addTypePack({iteratorType});
asMutable(context.result)->ty.emplace<BoundTypePack>(resTypePack);
return true;
}
static std::optional<WithPredicate<TypePackId>> magicFunctionMatch(
TypeChecker& typechecker, const ScopePtr& scope, const AstExprCall& expr, WithPredicate<TypePackId> withPredicate)
{
auto [paramPack, _predicates] = withPredicate;
const auto& [params, tail] = flatten(paramPack);
if (params.size() < 2 || params.size() > 3)
return std::nullopt;
TypeArena& arena = typechecker.currentModule->internalTypes;
AstExprConstantString* pattern = nullptr;
size_t patternIndex = expr.self ? 0 : 1;
if (expr.args.size > patternIndex)
pattern = expr.args.data[patternIndex]->as<AstExprConstantString>();
if (!pattern)
return std::nullopt;
std::vector<TypeId> returnTypes = parsePatternString(typechecker.builtinTypes, pattern->value.data, pattern->value.size);
if (returnTypes.empty())
return std::nullopt;
typechecker.unify(params[0], typechecker.stringType, scope, expr.args.data[0]->location);
const TypeId optionalNumber = arena.addType(UnionType{{typechecker.nilType, typechecker.numberType}});
size_t initIndex = expr.self ? 1 : 2;
if (params.size() == 3 && expr.args.size > initIndex)
typechecker.unify(params[2], optionalNumber, scope, expr.args.data[initIndex]->location);
const TypePackId returnList = arena.addTypePack(returnTypes);
return WithPredicate<TypePackId>{returnList};
}
static bool dcrMagicFunctionMatch(MagicFunctionCallContext context)
{
const auto& [params, tail] = flatten(context.arguments);
if (params.size() < 2 || params.size() > 3)
return false;
TypeArena* arena = context.solver->arena;
AstExprConstantString* pattern = nullptr;
size_t patternIndex = context.callSite->self ? 0 : 1;
if (context.callSite->args.size > patternIndex)
pattern = context.callSite->args.data[patternIndex]->as<AstExprConstantString>();
if (!pattern)
return false;
std::vector<TypeId> returnTypes = parsePatternString(context.solver->builtinTypes, pattern->value.data, pattern->value.size);
if (returnTypes.empty())
return false;
context.solver->unify(context.solver->rootScope, context.callSite->location, params[0], context.solver->builtinTypes->stringType);
const TypeId optionalNumber = arena->addType(UnionType{{context.solver->builtinTypes->nilType, context.solver->builtinTypes->numberType}});
size_t initIndex = context.callSite->self ? 1 : 2;
if (params.size() == 3 && context.callSite->args.size > initIndex)
context.solver->unify(context.solver->rootScope, context.callSite->location, params[2], optionalNumber);
const TypePackId returnList = arena->addTypePack(returnTypes);
asMutable(context.result)->ty.emplace<BoundTypePack>(returnList);
return true;
}
static std::optional<WithPredicate<TypePackId>> magicFunctionFind(
TypeChecker& typechecker, const ScopePtr& scope, const AstExprCall& expr, WithPredicate<TypePackId> withPredicate)
{
auto [paramPack, _predicates] = withPredicate;
const auto& [params, tail] = flatten(paramPack);
if (params.size() < 2 || params.size() > 4)
return std::nullopt;
TypeArena& arena = typechecker.currentModule->internalTypes;
AstExprConstantString* pattern = nullptr;
size_t patternIndex = expr.self ? 0 : 1;
if (expr.args.size > patternIndex)
pattern = expr.args.data[patternIndex]->as<AstExprConstantString>();
if (!pattern)
return std::nullopt;
bool plain = false;
size_t plainIndex = expr.self ? 2 : 3;
if (expr.args.size > plainIndex)
{
AstExprConstantBool* p = expr.args.data[plainIndex]->as<AstExprConstantBool>();
plain = p && p->value;
}
std::vector<TypeId> returnTypes;
if (!plain)
{
returnTypes = parsePatternString(typechecker.builtinTypes, pattern->value.data, pattern->value.size);
if (returnTypes.empty())
return std::nullopt;
}
typechecker.unify(params[0], typechecker.stringType, scope, expr.args.data[0]->location);
const TypeId optionalNumber = arena.addType(UnionType{{typechecker.nilType, typechecker.numberType}});
const TypeId optionalBoolean = arena.addType(UnionType{{typechecker.nilType, typechecker.booleanType}});
size_t initIndex = expr.self ? 1 : 2;
if (params.size() >= 3 && expr.args.size > initIndex)
typechecker.unify(params[2], optionalNumber, scope, expr.args.data[initIndex]->location);
if (params.size() == 4 && expr.args.size > plainIndex)
typechecker.unify(params[3], optionalBoolean, scope, expr.args.data[plainIndex]->location);
returnTypes.insert(returnTypes.begin(), {optionalNumber, optionalNumber});
const TypePackId returnList = arena.addTypePack(returnTypes);
return WithPredicate<TypePackId>{returnList};
}
static bool dcrMagicFunctionFind(MagicFunctionCallContext context)
{
const auto& [params, tail] = flatten(context.arguments);
if (params.size() < 2 || params.size() > 4)
return false;
TypeArena* arena = context.solver->arena;
NotNull<BuiltinTypes> builtinTypes = context.solver->builtinTypes;
AstExprConstantString* pattern = nullptr;
size_t patternIndex = context.callSite->self ? 0 : 1;
if (context.callSite->args.size > patternIndex)
pattern = context.callSite->args.data[patternIndex]->as<AstExprConstantString>();
if (!pattern)
return false;
bool plain = false;
size_t plainIndex = context.callSite->self ? 2 : 3;
if (context.callSite->args.size > plainIndex)
{
AstExprConstantBool* p = context.callSite->args.data[plainIndex]->as<AstExprConstantBool>();
plain = p && p->value;
}
std::vector<TypeId> returnTypes;
if (!plain)
{
returnTypes = parsePatternString(builtinTypes, pattern->value.data, pattern->value.size);
if (returnTypes.empty())
return false;
}
context.solver->unify(context.solver->rootScope, context.callSite->location, params[0], builtinTypes->stringType);
const TypeId optionalNumber = arena->addType(UnionType{{builtinTypes->nilType, builtinTypes->numberType}});
const TypeId optionalBoolean = arena->addType(UnionType{{builtinTypes->nilType, builtinTypes->booleanType}});
size_t initIndex = context.callSite->self ? 1 : 2;
if (params.size() >= 3 && context.callSite->args.size > initIndex)
context.solver->unify(context.solver->rootScope, context.callSite->location, params[2], optionalNumber);
if (params.size() == 4 && context.callSite->args.size > plainIndex)
context.solver->unify(context.solver->rootScope, context.callSite->location, params[3], optionalBoolean);
returnTypes.insert(returnTypes.begin(), {optionalNumber, optionalNumber});
const TypePackId returnList = arena->addTypePack(returnTypes);
asMutable(context.result)->ty.emplace<BoundTypePack>(returnList);
return true;
}
TypeId makeStringMetatable(NotNull<BuiltinTypes> builtinTypes)
{
NotNull<TypeArena> arena{builtinTypes->arena.get()};
const TypeId nilType = builtinTypes->nilType;
const TypeId numberType = builtinTypes->numberType;
const TypeId booleanType = builtinTypes->booleanType;
const TypeId stringType = builtinTypes->stringType;
const TypeId anyType = builtinTypes->anyType;
const TypeId optionalNumber = arena->addType(UnionType{{nilType, numberType}});
const TypeId optionalString = arena->addType(UnionType{{nilType, stringType}});
const TypeId optionalBoolean = arena->addType(UnionType{{nilType, booleanType}});
const TypePackId oneStringPack = arena->addTypePack({stringType});
const TypePackId anyTypePack = arena->addTypePack(TypePackVar{VariadicTypePack{anyType}, true});
FunctionType formatFTV{arena->addTypePack(TypePack{{stringType}, anyTypePack}), oneStringPack};
formatFTV.magicFunction = &magicFunctionFormat;
const TypeId formatFn = arena->addType(formatFTV);
attachDcrMagicFunction(formatFn, dcrMagicFunctionFormat);
const TypePackId emptyPack = arena->addTypePack({});
const TypePackId stringVariadicList = arena->addTypePack(TypePackVar{VariadicTypePack{stringType}});
const TypePackId numberVariadicList = arena->addTypePack(TypePackVar{VariadicTypePack{numberType}});
const TypeId stringToStringType = makeFunction(*arena, std::nullopt, {}, {}, {stringType}, {}, {stringType});
const TypeId replArgType =
arena->addType(UnionType{{stringType, arena->addType(TableType({}, TableIndexer(stringType, stringType), TypeLevel{}, TableState::Generic)),
makeFunction(*arena, std::nullopt, {}, {}, {stringType}, {}, {stringType})}});
const TypeId gsubFunc = makeFunction(*arena, stringType, {}, {}, {stringType, replArgType, optionalNumber}, {}, {stringType, numberType});
const TypeId gmatchFunc =
makeFunction(*arena, stringType, {}, {}, {stringType}, {}, {arena->addType(FunctionType{emptyPack, stringVariadicList})});
attachMagicFunction(gmatchFunc, magicFunctionGmatch);
attachDcrMagicFunction(gmatchFunc, dcrMagicFunctionGmatch);
const TypeId matchFunc = arena->addType(
FunctionType{arena->addTypePack({stringType, stringType, optionalNumber}), arena->addTypePack(TypePackVar{VariadicTypePack{stringType}})});
attachMagicFunction(matchFunc, magicFunctionMatch);
attachDcrMagicFunction(matchFunc, dcrMagicFunctionMatch);
const TypeId findFunc = arena->addType(FunctionType{arena->addTypePack({stringType, stringType, optionalNumber, optionalBoolean}),
arena->addTypePack(TypePack{{optionalNumber, optionalNumber}, stringVariadicList})});
attachMagicFunction(findFunc, magicFunctionFind);
attachDcrMagicFunction(findFunc, dcrMagicFunctionFind);
TableType::Props stringLib = {
{"byte", {arena->addType(FunctionType{arena->addTypePack({stringType, optionalNumber, optionalNumber}), numberVariadicList})}},
{"char", {arena->addType(FunctionType{numberVariadicList, arena->addTypePack({stringType})})}},
{"find", {findFunc}},
{"format", {formatFn}}, // FIXME
{"gmatch", {gmatchFunc}},
{"gsub", {gsubFunc}},
{"len", {makeFunction(*arena, stringType, {}, {}, {}, {}, {numberType})}},
{"lower", {stringToStringType}},
{"match", {matchFunc}},
{"rep", {makeFunction(*arena, stringType, {}, {}, {numberType}, {}, {stringType})}},
{"reverse", {stringToStringType}},
{"sub", {makeFunction(*arena, stringType, {}, {}, {numberType, optionalNumber}, {}, {stringType})}},
{"upper", {stringToStringType}},
{"split", {makeFunction(*arena, stringType, {}, {}, {optionalString}, {},
{arena->addType(TableType{{}, TableIndexer{numberType, stringType}, TypeLevel{}, TableState::Sealed})})}},
{"pack", {arena->addType(FunctionType{
arena->addTypePack(TypePack{{stringType}, anyTypePack}),
oneStringPack,
})}},
{"packsize", {makeFunction(*arena, stringType, {}, {}, {}, {}, {numberType})}},
{"unpack", {arena->addType(FunctionType{
arena->addTypePack(TypePack{{stringType, stringType, optionalNumber}}),
anyTypePack,
})}},
};
assignPropDocumentationSymbols(stringLib, "@luau/global/string");
TypeId tableType = arena->addType(TableType{std::move(stringLib), std::nullopt, TypeLevel{}, TableState::Sealed});
if (TableType* ttv = getMutable<TableType>(tableType))
ttv->name = "typeof(string)";
return arena->addType(TableType{{{{"__index", {tableType}}}}, std::nullopt, TypeLevel{}, TableState::Sealed});
}
static std::optional<WithPredicate<TypePackId>> magicFunctionSelect(
TypeChecker& typechecker, const ScopePtr& scope, const AstExprCall& expr, WithPredicate<TypePackId> withPredicate)
{

View File

@ -36,6 +36,7 @@ LUAU_FASTFLAGVARIABLE(DebugLuauDeferredConstraintResolution, false)
LUAU_FASTFLAGVARIABLE(DebugLuauLogSolverToJson, false)
LUAU_FASTFLAGVARIABLE(DebugLuauReadWriteProperties, false)
LUAU_FASTFLAGVARIABLE(LuauTypecheckLimitControls, false)
LUAU_FASTFLAGVARIABLE(CorrectEarlyReturnInMarkDirty, false)
namespace Luau
{
@ -928,7 +929,6 @@ void Frontend::checkBuildQueueItem(BuildQueueItem& item)
{
// The autocomplete typecheck is always in strict mode with DM awareness
// to provide better type information for IDE features
TypeCheckLimits typeCheckLimits;
if (autocompleteTimeLimit != 0.0)
typeCheckLimits.finishTime = TimeTrace::getClock() + autocompleteTimeLimit;
@ -1149,8 +1149,16 @@ bool Frontend::isDirty(const ModuleName& name, bool forAutocomplete) const
*/
void Frontend::markDirty(const ModuleName& name, std::vector<ModuleName>* markedDirty)
{
if (!moduleResolver.getModule(name) && !moduleResolverForAutocomplete.getModule(name))
return;
if (FFlag::CorrectEarlyReturnInMarkDirty)
{
if (sourceNodes.count(name) == 0)
return;
}
else
{
if (!moduleResolver.getModule(name) && !moduleResolverForAutocomplete.getModule(name))
return;
}
std::unordered_map<ModuleName, std::vector<ModuleName>> reverseDeps;
for (const auto& module : sourceNodes)

View File

@ -0,0 +1,34 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#include "Luau/GlobalTypes.h"
LUAU_FASTFLAG(LuauInitializeStringMetatableInGlobalTypes)
namespace Luau
{
GlobalTypes::GlobalTypes(NotNull<BuiltinTypes> builtinTypes)
: builtinTypes(builtinTypes)
{
globalScope = std::make_shared<Scope>(globalTypes.addTypePack(TypePackVar{FreeTypePack{TypeLevel{}}}));
globalScope->addBuiltinTypeBinding("any", TypeFun{{}, builtinTypes->anyType});
globalScope->addBuiltinTypeBinding("nil", TypeFun{{}, builtinTypes->nilType});
globalScope->addBuiltinTypeBinding("number", TypeFun{{}, builtinTypes->numberType});
globalScope->addBuiltinTypeBinding("string", TypeFun{{}, builtinTypes->stringType});
globalScope->addBuiltinTypeBinding("boolean", TypeFun{{}, builtinTypes->booleanType});
globalScope->addBuiltinTypeBinding("thread", TypeFun{{}, builtinTypes->threadType});
globalScope->addBuiltinTypeBinding("unknown", TypeFun{{}, builtinTypes->unknownType});
globalScope->addBuiltinTypeBinding("never", TypeFun{{}, builtinTypes->neverType});
if (FFlag::LuauInitializeStringMetatableInGlobalTypes)
{
unfreeze(*builtinTypes->arena);
TypeId stringMetatableTy = makeStringMetatable(builtinTypes);
asMutable(builtinTypes->stringType)->ty.emplace<PrimitiveType>(PrimitiveType::String, stringMetatableTy);
persist(stringMetatableTy);
freeze(*builtinTypes->arena);
}
}
}

View File

@ -664,9 +664,8 @@ SubtypingResult Subtyping::isSubtype_(const NormalizedType* subNorm, const Norma
result.andAlso(isSubtype_(subNorm->tables, superNorm->tables));
// isSubtype_(subNorm->tables, superNorm->strings);
// isSubtype_(subNorm->tables, superNorm->classes);
// isSubtype_(subNorm->functions, superNorm->functions);
result.andAlso(isSubtype_(subNorm->functions, superNorm->functions));
// isSubtype_(subNorm->tyvars, superNorm->tyvars);
return result;
}
@ -703,6 +702,16 @@ SubtypingResult Subtyping::isSubtype_(const NormalizedClassType& subClass, const
return {true};
}
SubtypingResult Subtyping::isSubtype_(const NormalizedFunctionType& subFunction, const NormalizedFunctionType& superFunction)
{
if (subFunction.isNever())
return {true};
else if (superFunction.isTop)
return {true};
else
return isSubtype_(subFunction.parts, superFunction.parts);
}
SubtypingResult Subtyping::isSubtype_(const TypeIds& subTypes, const TypeIds& superTypes)
{
std::vector<SubtypingResult> results;

View File

@ -9,6 +9,8 @@
#include <unordered_map>
#include <unordered_set>
LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution);
namespace Luau
{
@ -52,7 +54,7 @@ bool StateDot::canDuplicatePrimitive(TypeId ty)
if (get<BoundType>(ty))
return false;
return get<PrimitiveType>(ty) || get<AnyType>(ty);
return get<PrimitiveType>(ty) || get<AnyType>(ty) || get<UnknownType>(ty) || get<NeverType>(ty);
}
void StateDot::visitChild(TypeId ty, int parentIndex, const char* linkName)
@ -76,6 +78,10 @@ void StateDot::visitChild(TypeId ty, int parentIndex, const char* linkName)
formatAppend(result, "n%d [label=\"%s\"];\n", index, toString(ty).c_str());
else if (get<AnyType>(ty))
formatAppend(result, "n%d [label=\"any\"];\n", index);
else if (get<UnknownType>(ty))
formatAppend(result, "n%d [label=\"unknown\"];\n", index);
else if (get<NeverType>(ty))
formatAppend(result, "n%d [label=\"never\"];\n", index);
}
else
{
@ -139,159 +145,215 @@ void StateDot::visitChildren(TypeId ty, int index)
startNode(index);
startNodeLabel();
if (const BoundType* btv = get<BoundType>(ty))
auto go = [&](auto&& t)
{
formatAppend(result, "BoundType %d", index);
finishNodeLabel(ty);
finishNode();
using T = std::decay_t<decltype(t)>;
visitChild(btv->boundTo, index);
}
else if (const FunctionType* ftv = get<FunctionType>(ty))
{
formatAppend(result, "FunctionType %d", index);
finishNodeLabel(ty);
finishNode();
visitChild(ftv->argTypes, index, "arg");
visitChild(ftv->retTypes, index, "ret");
}
else if (const TableType* ttv = get<TableType>(ty))
{
if (ttv->name)
formatAppend(result, "TableType %s", ttv->name->c_str());
else if (ttv->syntheticName)
formatAppend(result, "TableType %s", ttv->syntheticName->c_str());
else
formatAppend(result, "TableType %d", index);
finishNodeLabel(ty);
finishNode();
if (ttv->boundTo)
return visitChild(*ttv->boundTo, index, "boundTo");
for (const auto& [name, prop] : ttv->props)
visitChild(prop.type(), index, name.c_str());
if (ttv->indexer)
if constexpr (std::is_same_v<T, BoundType>)
{
visitChild(ttv->indexer->indexType, index, "[index]");
visitChild(ttv->indexer->indexResultType, index, "[value]");
formatAppend(result, "BoundType %d", index);
finishNodeLabel(ty);
finishNode();
visitChild(t.boundTo, index);
}
for (TypeId itp : ttv->instantiatedTypeParams)
visitChild(itp, index, "typeParam");
for (TypePackId itp : ttv->instantiatedTypePackParams)
visitChild(itp, index, "typePackParam");
}
else if (const MetatableType* mtv = get<MetatableType>(ty))
{
formatAppend(result, "MetatableType %d", index);
finishNodeLabel(ty);
finishNode();
visitChild(mtv->table, index, "table");
visitChild(mtv->metatable, index, "metatable");
}
else if (const UnionType* utv = get<UnionType>(ty))
{
formatAppend(result, "UnionType %d", index);
finishNodeLabel(ty);
finishNode();
for (TypeId opt : utv->options)
visitChild(opt, index);
}
else if (const IntersectionType* itv = get<IntersectionType>(ty))
{
formatAppend(result, "IntersectionType %d", index);
finishNodeLabel(ty);
finishNode();
for (TypeId part : itv->parts)
visitChild(part, index);
}
else if (const GenericType* gtv = get<GenericType>(ty))
{
if (gtv->explicitName)
formatAppend(result, "GenericType %s", gtv->name.c_str());
else
formatAppend(result, "GenericType %d", index);
finishNodeLabel(ty);
finishNode();
}
else if (const FreeType* ftv = get<FreeType>(ty))
{
formatAppend(result, "FreeType %d", index);
finishNodeLabel(ty);
finishNode();
}
else if (get<AnyType>(ty))
{
formatAppend(result, "AnyType %d", index);
finishNodeLabel(ty);
finishNode();
}
else if (get<PrimitiveType>(ty))
{
formatAppend(result, "PrimitiveType %s", toString(ty).c_str());
finishNodeLabel(ty);
finishNode();
}
else if (get<ErrorType>(ty))
{
formatAppend(result, "ErrorType %d", index);
finishNodeLabel(ty);
finishNode();
}
else if (const ClassType* ctv = get<ClassType>(ty))
{
formatAppend(result, "ClassType %s", ctv->name.c_str());
finishNodeLabel(ty);
finishNode();
for (const auto& [name, prop] : ctv->props)
visitChild(prop.type(), index, name.c_str());
if (ctv->parent)
visitChild(*ctv->parent, index, "[parent]");
if (ctv->metatable)
visitChild(*ctv->metatable, index, "[metatable]");
if (ctv->indexer)
else if constexpr (std::is_same_v<T, BlockedType>)
{
visitChild(ctv->indexer->indexType, index, "[index]");
visitChild(ctv->indexer->indexResultType, index, "[value]");
formatAppend(result, "BlockedType %d", index);
finishNodeLabel(ty);
finishNode();
}
}
else if (const SingletonType* stv = get<SingletonType>(ty))
{
std::string res;
else if constexpr (std::is_same_v<T, FunctionType>)
{
formatAppend(result, "FunctionType %d", index);
finishNodeLabel(ty);
finishNode();
if (const StringSingleton* ss = get<StringSingleton>(stv))
{
// Don't put in quotes anywhere. If it's outside of the call to escape,
// then it's invalid syntax. If it's inside, then escaping is super noisy.
res = "string: " + escape(ss->value);
visitChild(t.argTypes, index, "arg");
visitChild(t.retTypes, index, "ret");
}
else if (const BooleanSingleton* bs = get<BooleanSingleton>(stv))
else if constexpr (std::is_same_v<T, TableType>)
{
res = "boolean: ";
res += bs->value ? "true" : "false";
if (t.name)
formatAppend(result, "TableType %s", t.name->c_str());
else if (t.syntheticName)
formatAppend(result, "TableType %s", t.syntheticName->c_str());
else
formatAppend(result, "TableType %d", index);
finishNodeLabel(ty);
finishNode();
if (t.boundTo)
return visitChild(*t.boundTo, index, "boundTo");
for (const auto& [name, prop] : t.props)
visitChild(prop.type(), index, name.c_str());
if (t.indexer)
{
visitChild(t.indexer->indexType, index, "[index]");
visitChild(t.indexer->indexResultType, index, "[value]");
}
for (TypeId itp : t.instantiatedTypeParams)
visitChild(itp, index, "typeParam");
for (TypePackId itp : t.instantiatedTypePackParams)
visitChild(itp, index, "typePackParam");
}
else if constexpr (std::is_same_v<T, MetatableType>)
{
formatAppend(result, "MetatableType %d", index);
finishNodeLabel(ty);
finishNode();
visitChild(t.table, index, "table");
visitChild(t.metatable, index, "metatable");
}
else if constexpr (std::is_same_v<T, UnionType>)
{
formatAppend(result, "UnionType %d", index);
finishNodeLabel(ty);
finishNode();
for (TypeId opt : t.options)
visitChild(opt, index);
}
else if constexpr (std::is_same_v<T, IntersectionType>)
{
formatAppend(result, "IntersectionType %d", index);
finishNodeLabel(ty);
finishNode();
for (TypeId part : t.parts)
visitChild(part, index);
}
else if constexpr (std::is_same_v<T, LazyType>)
{
formatAppend(result, "LazyType %d", index);
finishNodeLabel(ty);
finishNode();
}
else if constexpr (std::is_same_v<T, PendingExpansionType>)
{
formatAppend(result, "PendingExpansionType %d", index);
finishNodeLabel(ty);
finishNode();
}
else if constexpr (std::is_same_v<T, GenericType>)
{
if (t.explicitName)
formatAppend(result, "GenericType %s", t.name.c_str());
else
formatAppend(result, "GenericType %d", index);
finishNodeLabel(ty);
finishNode();
}
else if constexpr (std::is_same_v<T, FreeType>)
{
formatAppend(result, "FreeType %d", index);
finishNodeLabel(ty);
finishNode();
if (FFlag::DebugLuauDeferredConstraintResolution)
{
if (!get<NeverType>(t.lowerBound))
visitChild(t.lowerBound, index, "[lowerBound]");
if (!get<UnknownType>(t.upperBound))
visitChild(t.upperBound, index, "[upperBound]");
}
}
else if constexpr (std::is_same_v<T, AnyType>)
{
formatAppend(result, "AnyType %d", index);
finishNodeLabel(ty);
finishNode();
}
else if constexpr (std::is_same_v<T, UnknownType>)
{
formatAppend(result, "UnknownType %d", index);
finishNodeLabel(ty);
finishNode();
}
else if constexpr (std::is_same_v<T, NeverType>)
{
formatAppend(result, "NeverType %d", index);
finishNodeLabel(ty);
finishNode();
}
else if constexpr (std::is_same_v<T, PrimitiveType>)
{
formatAppend(result, "PrimitiveType %s", toString(ty).c_str());
finishNodeLabel(ty);
finishNode();
}
else if constexpr (std::is_same_v<T, ErrorType>)
{
formatAppend(result, "ErrorType %d", index);
finishNodeLabel(ty);
finishNode();
}
else if constexpr (std::is_same_v<T, ClassType>)
{
formatAppend(result, "ClassType %s", t.name.c_str());
finishNodeLabel(ty);
finishNode();
for (const auto& [name, prop] : t.props)
visitChild(prop.type(), index, name.c_str());
if (t.parent)
visitChild(*t.parent, index, "[parent]");
if (t.metatable)
visitChild(*t.metatable, index, "[metatable]");
if (t.indexer)
{
visitChild(t.indexer->indexType, index, "[index]");
visitChild(t.indexer->indexResultType, index, "[value]");
}
}
else if constexpr (std::is_same_v<T, SingletonType>)
{
std::string res;
if (const StringSingleton* ss = get<StringSingleton>(&t))
{
// Don't put in quotes anywhere. If it's outside of the call to escape,
// then it's invalid syntax. If it's inside, then escaping is super noisy.
res = "string: " + escape(ss->value);
}
else if (const BooleanSingleton* bs = get<BooleanSingleton>(&t))
{
res = "boolean: ";
res += bs->value ? "true" : "false";
}
else
LUAU_ASSERT(!"unknown singleton type");
formatAppend(result, "SingletonType %s", res.c_str());
finishNodeLabel(ty);
finishNode();
}
else if constexpr (std::is_same_v<T, NegationType>)
{
formatAppend(result, "NegationType %d", index);
finishNodeLabel(ty);
finishNode();
visitChild(t.ty, index, "[negated]");
}
else if constexpr (std::is_same_v<T, TypeFamilyInstanceType>)
{
formatAppend(result, "TypeFamilyInstanceType %d", index);
finishNodeLabel(ty);
finishNode();
}
else
LUAU_ASSERT(!"unknown singleton type");
static_assert(always_false_v<T>, "unknown type kind");
};
formatAppend(result, "SingletonType %s", res.c_str());
finishNodeLabel(ty);
finishNode();
}
else
{
LUAU_ASSERT(!"unknown type kind");
finishNodeLabel(ty);
finishNode();
}
visit(go, ty->ty);
}
void StateDot::visitChildren(TypePackId tp, int index)

View File

@ -27,26 +27,11 @@ LUAU_FASTINT(LuauTypeInferRecursionLimit)
LUAU_FASTFLAG(LuauInstantiateInSubtyping)
LUAU_FASTFLAG(LuauNormalizeBlockedTypes)
LUAU_FASTFLAG(DebugLuauReadWriteProperties)
LUAU_FASTFLAGVARIABLE(LuauInitializeStringMetatableInGlobalTypes, false)
namespace Luau
{
std::optional<WithPredicate<TypePackId>> magicFunctionFormat(
TypeChecker& typechecker, const ScopePtr& scope, const AstExprCall& expr, WithPredicate<TypePackId> withPredicate);
static bool dcrMagicFunctionFormat(MagicFunctionCallContext context);
static std::optional<WithPredicate<TypePackId>> magicFunctionGmatch(
TypeChecker& typechecker, const ScopePtr& scope, const AstExprCall& expr, WithPredicate<TypePackId> withPredicate);
static bool dcrMagicFunctionGmatch(MagicFunctionCallContext context);
static std::optional<WithPredicate<TypePackId>> magicFunctionMatch(
TypeChecker& typechecker, const ScopePtr& scope, const AstExprCall& expr, WithPredicate<TypePackId> withPredicate);
static bool dcrMagicFunctionMatch(MagicFunctionCallContext context);
static std::optional<WithPredicate<TypePackId>> magicFunctionFind(
TypeChecker& typechecker, const ScopePtr& scope, const AstExprCall& expr, WithPredicate<TypePackId> withPredicate);
static bool dcrMagicFunctionFind(MagicFunctionCallContext context);
// LUAU_NOINLINE prevents unwrapLazy from being inlined into advance below; advance is important to keep inlineable
static LUAU_NOINLINE TypeId unwrapLazy(LazyType* ltv)
{
@ -933,6 +918,8 @@ TypeId makeFunction(TypeArena& arena, std::optional<TypeId> selfType, std::initi
std::initializer_list<TypePackId> genericPacks, std::initializer_list<TypeId> paramTypes, std::initializer_list<std::string> paramNames,
std::initializer_list<TypeId> retTypes);
TypeId makeStringMetatable(NotNull<BuiltinTypes> builtinTypes); // BuiltinDefinitions.cpp
BuiltinTypes::BuiltinTypes()
: arena(new TypeArena)
, debugFreezeArena(FFlag::DebugLuauFreezeArena)
@ -961,9 +948,12 @@ BuiltinTypes::BuiltinTypes()
, uninhabitableTypePack(arena->addTypePack(TypePackVar{TypePack{{neverType}, neverTypePack}, /*persistent*/ true}))
, errorTypePack(arena->addTypePack(TypePackVar{Unifiable::Error{}, /*persistent*/ true}))
{
TypeId stringMetatable = makeStringMetatable();
asMutable(stringType)->ty = PrimitiveType{PrimitiveType::String, stringMetatable};
persist(stringMetatable);
if (!FFlag::LuauInitializeStringMetatableInGlobalTypes)
{
TypeId stringMetatable = makeStringMetatable(NotNull{this});
asMutable(stringType)->ty = PrimitiveType{PrimitiveType::String, stringMetatable};
persist(stringMetatable);
}
freeze(*arena);
}
@ -980,82 +970,6 @@ BuiltinTypes::~BuiltinTypes()
FFlag::DebugLuauFreezeArena.value = prevFlag;
}
TypeId BuiltinTypes::makeStringMetatable()
{
const TypeId optionalNumber = arena->addType(UnionType{{nilType, numberType}});
const TypeId optionalString = arena->addType(UnionType{{nilType, stringType}});
const TypeId optionalBoolean = arena->addType(UnionType{{nilType, booleanType}});
const TypePackId oneStringPack = arena->addTypePack({stringType});
const TypePackId anyTypePack = arena->addTypePack(TypePackVar{VariadicTypePack{anyType}, true});
FunctionType formatFTV{arena->addTypePack(TypePack{{stringType}, anyTypePack}), oneStringPack};
formatFTV.magicFunction = &magicFunctionFormat;
const TypeId formatFn = arena->addType(formatFTV);
attachDcrMagicFunction(formatFn, dcrMagicFunctionFormat);
const TypePackId emptyPack = arena->addTypePack({});
const TypePackId stringVariadicList = arena->addTypePack(TypePackVar{VariadicTypePack{stringType}});
const TypePackId numberVariadicList = arena->addTypePack(TypePackVar{VariadicTypePack{numberType}});
const TypeId stringToStringType = makeFunction(*arena, std::nullopt, {}, {}, {stringType}, {}, {stringType});
const TypeId replArgType =
arena->addType(UnionType{{stringType, arena->addType(TableType({}, TableIndexer(stringType, stringType), TypeLevel{}, TableState::Generic)),
makeFunction(*arena, std::nullopt, {}, {}, {stringType}, {}, {stringType})}});
const TypeId gsubFunc = makeFunction(*arena, stringType, {}, {}, {stringType, replArgType, optionalNumber}, {}, {stringType, numberType});
const TypeId gmatchFunc =
makeFunction(*arena, stringType, {}, {}, {stringType}, {}, {arena->addType(FunctionType{emptyPack, stringVariadicList})});
attachMagicFunction(gmatchFunc, magicFunctionGmatch);
attachDcrMagicFunction(gmatchFunc, dcrMagicFunctionGmatch);
const TypeId matchFunc = arena->addType(
FunctionType{arena->addTypePack({stringType, stringType, optionalNumber}), arena->addTypePack(TypePackVar{VariadicTypePack{stringType}})});
attachMagicFunction(matchFunc, magicFunctionMatch);
attachDcrMagicFunction(matchFunc, dcrMagicFunctionMatch);
const TypeId findFunc = arena->addType(FunctionType{arena->addTypePack({stringType, stringType, optionalNumber, optionalBoolean}),
arena->addTypePack(TypePack{{optionalNumber, optionalNumber}, stringVariadicList})});
attachMagicFunction(findFunc, magicFunctionFind);
attachDcrMagicFunction(findFunc, dcrMagicFunctionFind);
TableType::Props stringLib = {
{"byte", {arena->addType(FunctionType{arena->addTypePack({stringType, optionalNumber, optionalNumber}), numberVariadicList})}},
{"char", {arena->addType(FunctionType{numberVariadicList, arena->addTypePack({stringType})})}},
{"find", {findFunc}},
{"format", {formatFn}}, // FIXME
{"gmatch", {gmatchFunc}},
{"gsub", {gsubFunc}},
{"len", {makeFunction(*arena, stringType, {}, {}, {}, {}, {numberType})}},
{"lower", {stringToStringType}},
{"match", {matchFunc}},
{"rep", {makeFunction(*arena, stringType, {}, {}, {numberType}, {}, {stringType})}},
{"reverse", {stringToStringType}},
{"sub", {makeFunction(*arena, stringType, {}, {}, {numberType, optionalNumber}, {}, {stringType})}},
{"upper", {stringToStringType}},
{"split", {makeFunction(*arena, stringType, {}, {}, {optionalString}, {},
{arena->addType(TableType{{}, TableIndexer{numberType, stringType}, TypeLevel{}, TableState::Sealed})})}},
{"pack", {arena->addType(FunctionType{
arena->addTypePack(TypePack{{stringType}, anyTypePack}),
oneStringPack,
})}},
{"packsize", {makeFunction(*arena, stringType, {}, {}, {}, {}, {numberType})}},
{"unpack", {arena->addType(FunctionType{
arena->addTypePack(TypePack{{stringType, stringType, optionalNumber}}),
anyTypePack,
})}},
};
assignPropDocumentationSymbols(stringLib, "@luau/global/string");
TypeId tableType = arena->addType(TableType{std::move(stringLib), std::nullopt, TypeLevel{}, TableState::Sealed});
if (TableType* ttv = getMutable<TableType>(tableType))
ttv->name = "typeof(string)";
return arena->addType(TableType{{{{"__index", {tableType}}}}, std::nullopt, TypeLevel{}, TableState::Sealed});
}
TypeId BuiltinTypes::errorRecoveryType() const
{
return errorType;
@ -1261,436 +1175,6 @@ IntersectionTypeIterator end(const IntersectionType* itv)
return IntersectionTypeIterator{};
}
static std::vector<TypeId> parseFormatString(NotNull<BuiltinTypes> builtinTypes, const char* data, size_t size)
{
const char* options = "cdiouxXeEfgGqs*";
std::vector<TypeId> result;
for (size_t i = 0; i < size; ++i)
{
if (data[i] == '%')
{
i++;
if (i < size && data[i] == '%')
continue;
// we just ignore all characters (including flags/precision) up until first alphabetic character
while (i < size && !(data[i] > 0 && (isalpha(data[i]) || data[i] == '*')))
i++;
if (i == size)
break;
if (data[i] == 'q' || data[i] == 's')
result.push_back(builtinTypes->stringType);
else if (data[i] == '*')
result.push_back(builtinTypes->unknownType);
else if (strchr(options, data[i]))
result.push_back(builtinTypes->numberType);
else
result.push_back(builtinTypes->errorRecoveryType(builtinTypes->anyType));
}
}
return result;
}
std::optional<WithPredicate<TypePackId>> magicFunctionFormat(
TypeChecker& typechecker, const ScopePtr& scope, const AstExprCall& expr, WithPredicate<TypePackId> withPredicate)
{
auto [paramPack, _predicates] = withPredicate;
TypeArena& arena = typechecker.currentModule->internalTypes;
AstExprConstantString* fmt = nullptr;
if (auto index = expr.func->as<AstExprIndexName>(); index && expr.self)
{
if (auto group = index->expr->as<AstExprGroup>())
fmt = group->expr->as<AstExprConstantString>();
else
fmt = index->expr->as<AstExprConstantString>();
}
if (!expr.self && expr.args.size > 0)
fmt = expr.args.data[0]->as<AstExprConstantString>();
if (!fmt)
return std::nullopt;
std::vector<TypeId> expected = parseFormatString(typechecker.builtinTypes, fmt->value.data, fmt->value.size);
const auto& [params, tail] = flatten(paramPack);
size_t paramOffset = 1;
size_t dataOffset = expr.self ? 0 : 1;
// unify the prefix one argument at a time
for (size_t i = 0; i < expected.size() && i + paramOffset < params.size(); ++i)
{
Location location = expr.args.data[std::min(i + dataOffset, expr.args.size - 1)]->location;
typechecker.unify(params[i + paramOffset], expected[i], scope, location);
}
// if we know the argument count or if we have too many arguments for sure, we can issue an error
size_t numActualParams = params.size();
size_t numExpectedParams = expected.size() + 1; // + 1 for the format string
if (numExpectedParams != numActualParams && (!tail || numExpectedParams < numActualParams))
typechecker.reportError(TypeError{expr.location, CountMismatch{numExpectedParams, std::nullopt, numActualParams}});
return WithPredicate<TypePackId>{arena.addTypePack({typechecker.stringType})};
}
static bool dcrMagicFunctionFormat(MagicFunctionCallContext context)
{
TypeArena* arena = context.solver->arena;
AstExprConstantString* fmt = nullptr;
if (auto index = context.callSite->func->as<AstExprIndexName>(); index && context.callSite->self)
{
if (auto group = index->expr->as<AstExprGroup>())
fmt = group->expr->as<AstExprConstantString>();
else
fmt = index->expr->as<AstExprConstantString>();
}
if (!context.callSite->self && context.callSite->args.size > 0)
fmt = context.callSite->args.data[0]->as<AstExprConstantString>();
if (!fmt)
return false;
std::vector<TypeId> expected = parseFormatString(context.solver->builtinTypes, fmt->value.data, fmt->value.size);
const auto& [params, tail] = flatten(context.arguments);
size_t paramOffset = 1;
// unify the prefix one argument at a time
for (size_t i = 0; i < expected.size() && i + paramOffset < params.size(); ++i)
{
context.solver->unify(context.solver->rootScope, context.callSite->location, params[i + paramOffset], expected[i]);
}
// if we know the argument count or if we have too many arguments for sure, we can issue an error
size_t numActualParams = params.size();
size_t numExpectedParams = expected.size() + 1; // + 1 for the format string
if (numExpectedParams != numActualParams && (!tail || numExpectedParams < numActualParams))
context.solver->reportError(TypeError{context.callSite->location, CountMismatch{numExpectedParams, std::nullopt, numActualParams}});
TypePackId resultPack = arena->addTypePack({context.solver->builtinTypes->stringType});
asMutable(context.result)->ty.emplace<BoundTypePack>(resultPack);
return true;
}
static std::vector<TypeId> parsePatternString(NotNull<BuiltinTypes> builtinTypes, const char* data, size_t size)
{
std::vector<TypeId> result;
int depth = 0;
bool parsingSet = false;
for (size_t i = 0; i < size; ++i)
{
if (data[i] == '%')
{
++i;
if (!parsingSet && i < size && data[i] == 'b')
i += 2;
}
else if (!parsingSet && data[i] == '[')
{
parsingSet = true;
if (i + 1 < size && data[i + 1] == ']')
i += 1;
}
else if (parsingSet && data[i] == ']')
{
parsingSet = false;
}
else if (data[i] == '(')
{
if (parsingSet)
continue;
if (i + 1 < size && data[i + 1] == ')')
{
i++;
result.push_back(builtinTypes->optionalNumberType);
continue;
}
++depth;
result.push_back(builtinTypes->optionalStringType);
}
else if (data[i] == ')')
{
if (parsingSet)
continue;
--depth;
if (depth < 0)
break;
}
}
if (depth != 0 || parsingSet)
return std::vector<TypeId>();
if (result.empty())
result.push_back(builtinTypes->optionalStringType);
return result;
}
static std::optional<WithPredicate<TypePackId>> magicFunctionGmatch(
TypeChecker& typechecker, const ScopePtr& scope, const AstExprCall& expr, WithPredicate<TypePackId> withPredicate)
{
auto [paramPack, _predicates] = withPredicate;
const auto& [params, tail] = flatten(paramPack);
if (params.size() != 2)
return std::nullopt;
TypeArena& arena = typechecker.currentModule->internalTypes;
AstExprConstantString* pattern = nullptr;
size_t index = expr.self ? 0 : 1;
if (expr.args.size > index)
pattern = expr.args.data[index]->as<AstExprConstantString>();
if (!pattern)
return std::nullopt;
std::vector<TypeId> returnTypes = parsePatternString(typechecker.builtinTypes, pattern->value.data, pattern->value.size);
if (returnTypes.empty())
return std::nullopt;
typechecker.unify(params[0], typechecker.stringType, scope, expr.args.data[0]->location);
const TypePackId emptyPack = arena.addTypePack({});
const TypePackId returnList = arena.addTypePack(returnTypes);
const TypeId iteratorType = arena.addType(FunctionType{emptyPack, returnList});
return WithPredicate<TypePackId>{arena.addTypePack({iteratorType})};
}
static bool dcrMagicFunctionGmatch(MagicFunctionCallContext context)
{
const auto& [params, tail] = flatten(context.arguments);
if (params.size() != 2)
return false;
TypeArena* arena = context.solver->arena;
AstExprConstantString* pattern = nullptr;
size_t index = context.callSite->self ? 0 : 1;
if (context.callSite->args.size > index)
pattern = context.callSite->args.data[index]->as<AstExprConstantString>();
if (!pattern)
return false;
std::vector<TypeId> returnTypes = parsePatternString(context.solver->builtinTypes, pattern->value.data, pattern->value.size);
if (returnTypes.empty())
return false;
context.solver->unify(context.solver->rootScope, context.callSite->location, params[0], context.solver->builtinTypes->stringType);
const TypePackId emptyPack = arena->addTypePack({});
const TypePackId returnList = arena->addTypePack(returnTypes);
const TypeId iteratorType = arena->addType(FunctionType{emptyPack, returnList});
const TypePackId resTypePack = arena->addTypePack({iteratorType});
asMutable(context.result)->ty.emplace<BoundTypePack>(resTypePack);
return true;
}
static std::optional<WithPredicate<TypePackId>> magicFunctionMatch(
TypeChecker& typechecker, const ScopePtr& scope, const AstExprCall& expr, WithPredicate<TypePackId> withPredicate)
{
auto [paramPack, _predicates] = withPredicate;
const auto& [params, tail] = flatten(paramPack);
if (params.size() < 2 || params.size() > 3)
return std::nullopt;
TypeArena& arena = typechecker.currentModule->internalTypes;
AstExprConstantString* pattern = nullptr;
size_t patternIndex = expr.self ? 0 : 1;
if (expr.args.size > patternIndex)
pattern = expr.args.data[patternIndex]->as<AstExprConstantString>();
if (!pattern)
return std::nullopt;
std::vector<TypeId> returnTypes = parsePatternString(typechecker.builtinTypes, pattern->value.data, pattern->value.size);
if (returnTypes.empty())
return std::nullopt;
typechecker.unify(params[0], typechecker.stringType, scope, expr.args.data[0]->location);
const TypeId optionalNumber = arena.addType(UnionType{{typechecker.nilType, typechecker.numberType}});
size_t initIndex = expr.self ? 1 : 2;
if (params.size() == 3 && expr.args.size > initIndex)
typechecker.unify(params[2], optionalNumber, scope, expr.args.data[initIndex]->location);
const TypePackId returnList = arena.addTypePack(returnTypes);
return WithPredicate<TypePackId>{returnList};
}
static bool dcrMagicFunctionMatch(MagicFunctionCallContext context)
{
const auto& [params, tail] = flatten(context.arguments);
if (params.size() < 2 || params.size() > 3)
return false;
TypeArena* arena = context.solver->arena;
AstExprConstantString* pattern = nullptr;
size_t patternIndex = context.callSite->self ? 0 : 1;
if (context.callSite->args.size > patternIndex)
pattern = context.callSite->args.data[patternIndex]->as<AstExprConstantString>();
if (!pattern)
return false;
std::vector<TypeId> returnTypes = parsePatternString(context.solver->builtinTypes, pattern->value.data, pattern->value.size);
if (returnTypes.empty())
return false;
context.solver->unify(context.solver->rootScope, context.callSite->location, params[0], context.solver->builtinTypes->stringType);
const TypeId optionalNumber = arena->addType(UnionType{{context.solver->builtinTypes->nilType, context.solver->builtinTypes->numberType}});
size_t initIndex = context.callSite->self ? 1 : 2;
if (params.size() == 3 && context.callSite->args.size > initIndex)
context.solver->unify(context.solver->rootScope, context.callSite->location, params[2], optionalNumber);
const TypePackId returnList = arena->addTypePack(returnTypes);
asMutable(context.result)->ty.emplace<BoundTypePack>(returnList);
return true;
}
static std::optional<WithPredicate<TypePackId>> magicFunctionFind(
TypeChecker& typechecker, const ScopePtr& scope, const AstExprCall& expr, WithPredicate<TypePackId> withPredicate)
{
auto [paramPack, _predicates] = withPredicate;
const auto& [params, tail] = flatten(paramPack);
if (params.size() < 2 || params.size() > 4)
return std::nullopt;
TypeArena& arena = typechecker.currentModule->internalTypes;
AstExprConstantString* pattern = nullptr;
size_t patternIndex = expr.self ? 0 : 1;
if (expr.args.size > patternIndex)
pattern = expr.args.data[patternIndex]->as<AstExprConstantString>();
if (!pattern)
return std::nullopt;
bool plain = false;
size_t plainIndex = expr.self ? 2 : 3;
if (expr.args.size > plainIndex)
{
AstExprConstantBool* p = expr.args.data[plainIndex]->as<AstExprConstantBool>();
plain = p && p->value;
}
std::vector<TypeId> returnTypes;
if (!plain)
{
returnTypes = parsePatternString(typechecker.builtinTypes, pattern->value.data, pattern->value.size);
if (returnTypes.empty())
return std::nullopt;
}
typechecker.unify(params[0], typechecker.stringType, scope, expr.args.data[0]->location);
const TypeId optionalNumber = arena.addType(UnionType{{typechecker.nilType, typechecker.numberType}});
const TypeId optionalBoolean = arena.addType(UnionType{{typechecker.nilType, typechecker.booleanType}});
size_t initIndex = expr.self ? 1 : 2;
if (params.size() >= 3 && expr.args.size > initIndex)
typechecker.unify(params[2], optionalNumber, scope, expr.args.data[initIndex]->location);
if (params.size() == 4 && expr.args.size > plainIndex)
typechecker.unify(params[3], optionalBoolean, scope, expr.args.data[plainIndex]->location);
returnTypes.insert(returnTypes.begin(), {optionalNumber, optionalNumber});
const TypePackId returnList = arena.addTypePack(returnTypes);
return WithPredicate<TypePackId>{returnList};
}
static bool dcrMagicFunctionFind(MagicFunctionCallContext context)
{
const auto& [params, tail] = flatten(context.arguments);
if (params.size() < 2 || params.size() > 4)
return false;
TypeArena* arena = context.solver->arena;
NotNull<BuiltinTypes> builtinTypes = context.solver->builtinTypes;
AstExprConstantString* pattern = nullptr;
size_t patternIndex = context.callSite->self ? 0 : 1;
if (context.callSite->args.size > patternIndex)
pattern = context.callSite->args.data[patternIndex]->as<AstExprConstantString>();
if (!pattern)
return false;
bool plain = false;
size_t plainIndex = context.callSite->self ? 2 : 3;
if (context.callSite->args.size > plainIndex)
{
AstExprConstantBool* p = context.callSite->args.data[plainIndex]->as<AstExprConstantBool>();
plain = p && p->value;
}
std::vector<TypeId> returnTypes;
if (!plain)
{
returnTypes = parsePatternString(builtinTypes, pattern->value.data, pattern->value.size);
if (returnTypes.empty())
return false;
}
context.solver->unify(context.solver->rootScope, context.callSite->location, params[0], builtinTypes->stringType);
const TypeId optionalNumber = arena->addType(UnionType{{builtinTypes->nilType, builtinTypes->numberType}});
const TypeId optionalBoolean = arena->addType(UnionType{{builtinTypes->nilType, builtinTypes->booleanType}});
size_t initIndex = context.callSite->self ? 1 : 2;
if (params.size() >= 3 && context.callSite->args.size > initIndex)
context.solver->unify(context.solver->rootScope, context.callSite->location, params[2], optionalNumber);
if (params.size() == 4 && context.callSite->args.size > plainIndex)
context.solver->unify(context.solver->rootScope, context.callSite->location, params[3], optionalBoolean);
returnTypes.insert(returnTypes.begin(), {optionalNumber, optionalNumber});
const TypePackId returnList = arena->addTypePack(returnTypes);
asMutable(context.result)->ty.emplace<BoundTypePack>(returnList);
return true;
}
TypeId freshType(NotNull<TypeArena> arena, NotNull<BuiltinTypes> builtinTypes, Scope* scope)
{
return arena->addType(FreeType{scope, builtinTypes->neverType, builtinTypes->unknownType});

View File

@ -38,6 +38,7 @@ LUAU_FASTFLAG(LuauInstantiateInSubtyping)
LUAU_FASTFLAGVARIABLE(LuauAllowIndexClassParameters, false)
LUAU_FASTFLAG(LuauOccursIsntAlwaysFailure)
LUAU_FASTFLAGVARIABLE(LuauTinyControlFlowAnalysis, false)
LUAU_FASTFLAGVARIABLE(LuauVariadicOverloadFix, false)
LUAU_FASTFLAGVARIABLE(LuauAlwaysCommitInferencesOfFunctionCalls, false)
LUAU_FASTFLAG(LuauParseDeclareClassIndexer)
LUAU_FASTFLAG(LuauFloorDivision);
@ -210,21 +211,6 @@ size_t HashBoolNamePair::operator()(const std::pair<bool, Name>& pair) const
return std::hash<bool>()(pair.first) ^ std::hash<Name>()(pair.second);
}
GlobalTypes::GlobalTypes(NotNull<BuiltinTypes> builtinTypes)
: builtinTypes(builtinTypes)
{
globalScope = std::make_shared<Scope>(globalTypes.addTypePack(TypePackVar{FreeTypePack{TypeLevel{}}}));
globalScope->addBuiltinTypeBinding("any", TypeFun{{}, builtinTypes->anyType});
globalScope->addBuiltinTypeBinding("nil", TypeFun{{}, builtinTypes->nilType});
globalScope->addBuiltinTypeBinding("number", TypeFun{{}, builtinTypes->numberType});
globalScope->addBuiltinTypeBinding("string", TypeFun{{}, builtinTypes->stringType});
globalScope->addBuiltinTypeBinding("boolean", TypeFun{{}, builtinTypes->booleanType});
globalScope->addBuiltinTypeBinding("thread", TypeFun{{}, builtinTypes->threadType});
globalScope->addBuiltinTypeBinding("unknown", TypeFun{{}, builtinTypes->unknownType});
globalScope->addBuiltinTypeBinding("never", TypeFun{{}, builtinTypes->neverType});
}
TypeChecker::TypeChecker(const ScopePtr& globalScope, ModuleResolver* resolver, NotNull<BuiltinTypes> builtinTypes, InternalErrorReporter* iceHandler)
: globalScope(globalScope)
, resolver(resolver)
@ -4038,7 +4024,13 @@ void TypeChecker::checkArgumentList(const ScopePtr& scope, const AstExpr& funNam
if (argIndex < argLocations.size())
location = argLocations[argIndex];
unify(*argIter, vtp->ty, scope, location);
if (FFlag::LuauVariadicOverloadFix)
{
state.location = location;
state.tryUnify(*argIter, vtp->ty);
}
else
unify(*argIter, vtp->ty, scope, location);
++argIter;
++argIndex;
}

View File

@ -25,7 +25,6 @@ LUAU_FASTFLAGVARIABLE(LuauOccursIsntAlwaysFailure, false)
LUAU_FASTFLAG(LuauNormalizeBlockedTypes)
LUAU_FASTFLAG(LuauAlwaysCommitInferencesOfFunctionCalls)
LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution)
LUAU_FASTFLAGVARIABLE(LuauTableUnifyRecursionLimit, false)
namespace Luau
{
@ -2260,23 +2259,13 @@ void Unifier::tryUnifyTables(TypeId subTy, TypeId superTy, bool isIntersection,
if (superTable != newSuperTable || subTable != newSubTable)
{
if (FFlag::LuauTableUnifyRecursionLimit)
if (errors.empty())
{
if (errors.empty())
{
RecursionLimiter _ra(&sharedState.counters.recursionCount, sharedState.counters.recursionLimit);
tryUnifyTables(subTy, superTy, isIntersection);
}
RecursionLimiter _ra(&sharedState.counters.recursionCount, sharedState.counters.recursionLimit);
tryUnifyTables(subTy, superTy, isIntersection);
}
return;
}
else
{
if (errors.empty())
return tryUnifyTables(subTy, superTy, isIntersection);
else
return;
}
return;
}
}
@ -2351,23 +2340,13 @@ void Unifier::tryUnifyTables(TypeId subTy, TypeId superTy, bool isIntersection,
if (superTable != newSuperTable || subTable != newSubTable)
{
if (FFlag::LuauTableUnifyRecursionLimit)
if (errors.empty())
{
if (errors.empty())
{
RecursionLimiter _ra(&sharedState.counters.recursionCount, sharedState.counters.recursionLimit);
tryUnifyTables(subTy, superTy, isIntersection);
}
RecursionLimiter _ra(&sharedState.counters.recursionCount, sharedState.counters.recursionLimit);
tryUnifyTables(subTy, superTy, isIntersection);
}
return;
}
else
{
if (errors.empty())
return tryUnifyTables(subTy, superTy, isIntersection);
else
return;
}
return;
}
}

View File

@ -7,7 +7,6 @@
#include <limits.h>
LUAU_FASTFLAGVARIABLE(LuauFloorDivision, false)
LUAU_FASTFLAGVARIABLE(LuauLexerConsumeFast, false)
LUAU_FASTFLAGVARIABLE(LuauLexerLookaheadRemembersBraceType, false)
namespace Luau
@ -460,19 +459,8 @@ Position Lexer::position() const
LUAU_FORCEINLINE
void Lexer::consume()
{
if (isNewline(buffer[offset]))
{
// TODO: When the flag is removed, remove the outer condition
if (FFlag::LuauLexerConsumeFast)
{
LUAU_ASSERT(!isNewline(buffer[offset]));
}
else
{
line++;
lineOffset = offset + 1;
}
}
// consume() assumes current character is known to not be a newline; use consumeAny if this is not guaranteed
LUAU_ASSERT(!isNewline(buffer[offset]));
offset++;
}

View File

@ -66,6 +66,8 @@ struct IrBuilder
bool inTerminatedBlock = false;
bool interruptRequested = false;
bool activeFastcallFallback = false;
IrOp fastcallFallbackReturn;
int fastcallSkipTarget = -1;
@ -76,6 +78,8 @@ struct IrBuilder
std::vector<uint32_t> instIndexToBlock; // Block index at the bytecode instruction
std::vector<IrOp> loopStepStack;
// Similar to BytecodeBuilder, duplicate constants are removed used the same method
struct ConstantKey
{

View File

@ -199,24 +199,12 @@ enum class IrCmd : uint8_t
// D: block (if false)
JUMP_EQ_TAG,
// Jump if two int numbers are equal
// A, B: int
// C: block (if true)
// D: block (if false)
JUMP_EQ_INT,
// Jump if A < B
// A, B: int
// C: block (if true)
// D: block (if false)
JUMP_LT_INT,
// Jump if unsigned(A) >= unsigned(B)
// Perform a conditional jump based on the result of integer comparison
// A, B: int
// C: condition
// D: block (if true)
// E: block (if false)
JUMP_GE_UINT,
JUMP_CMP_INT,
// Jump if pointers are equal
// A, B: pointer (*)

View File

@ -94,9 +94,7 @@ inline bool isBlockTerminator(IrCmd cmd)
case IrCmd::JUMP_IF_TRUTHY:
case IrCmd::JUMP_IF_FALSY:
case IrCmd::JUMP_EQ_TAG:
case IrCmd::JUMP_EQ_INT:
case IrCmd::JUMP_LT_INT:
case IrCmd::JUMP_GE_UINT:
case IrCmd::JUMP_CMP_INT:
case IrCmd::JUMP_EQ_POINTER:
case IrCmd::JUMP_CMP_NUM:
case IrCmd::JUMP_SLOT_MATCH:

View File

@ -12,6 +12,8 @@
#include "lgc.h"
#include "lstate.h"
#include <utility>
namespace Luau
{
namespace CodeGen
@ -22,10 +24,15 @@ namespace X64
void jumpOnNumberCmp(AssemblyBuilderX64& build, RegisterX64 tmp, OperandX64 lhs, OperandX64 rhs, IrCondition cond, Label& label)
{
// Refresher on comi/ucomi EFLAGS:
// all zero: greater
// CF only: less
// ZF only: equal
// PF+CF+ZF: unordered (NaN)
// To avoid the lack of conditional jumps that check for "greater" conditions in IEEE 754 compliant way, we use "less" forms to emulate these
if (cond == IrCondition::Greater || cond == IrCondition::GreaterEqual || cond == IrCondition::NotGreater || cond == IrCondition::NotGreaterEqual)
std::swap(lhs, rhs);
if (rhs.cat == CategoryX64::reg)
{
build.vucomisd(rhs, lhs);
@ -41,18 +48,22 @@ void jumpOnNumberCmp(AssemblyBuilderX64& build, RegisterX64 tmp, OperandX64 lhs,
switch (cond)
{
case IrCondition::NotLessEqual:
case IrCondition::NotGreaterEqual:
// (b < a) is the same as !(a <= b). jnae checks CF=1 which means < or NaN
build.jcc(ConditionX64::NotAboveEqual, label);
break;
case IrCondition::LessEqual:
case IrCondition::GreaterEqual:
// (b >= a) is the same as (a <= b). jae checks CF=0 which means >= and not NaN
build.jcc(ConditionX64::AboveEqual, label);
break;
case IrCondition::NotLess:
case IrCondition::NotGreater:
// (b <= a) is the same as !(a < b). jna checks CF=1 or ZF=1 which means <= or NaN
build.jcc(ConditionX64::NotAbove, label);
break;
case IrCondition::Less:
case IrCondition::Greater:
// (b > a) is the same as (a < b). ja checks CF=0 and ZF=0 which means > and not NaN
build.jcc(ConditionX64::Above, label);
break;
@ -66,6 +77,44 @@ void jumpOnNumberCmp(AssemblyBuilderX64& build, RegisterX64 tmp, OperandX64 lhs,
}
}
ConditionX64 getConditionInt(IrCondition cond)
{
switch (cond)
{
case IrCondition::Equal:
return ConditionX64::Equal;
case IrCondition::NotEqual:
return ConditionX64::NotEqual;
case IrCondition::Less:
return ConditionX64::Less;
case IrCondition::NotLess:
return ConditionX64::NotLess;
case IrCondition::LessEqual:
return ConditionX64::LessEqual;
case IrCondition::NotLessEqual:
return ConditionX64::NotLessEqual;
case IrCondition::Greater:
return ConditionX64::Greater;
case IrCondition::NotGreater:
return ConditionX64::NotGreater;
case IrCondition::GreaterEqual:
return ConditionX64::GreaterEqual;
case IrCondition::NotGreaterEqual:
return ConditionX64::NotGreaterEqual;
case IrCondition::UnsignedLess:
return ConditionX64::Below;
case IrCondition::UnsignedLessEqual:
return ConditionX64::BelowEqual;
case IrCondition::UnsignedGreater:
return ConditionX64::Above;
case IrCondition::UnsignedGreaterEqual:
return ConditionX64::AboveEqual;
default:
LUAU_ASSERT(!"Unsupported condition");
return ConditionX64::Zero;
}
}
void getTableNodeAtCachedSlot(AssemblyBuilderX64& build, RegisterX64 tmp, RegisterX64 node, RegisterX64 table, int pcpos)
{
LUAU_ASSERT(tmp != node);

View File

@ -195,6 +195,8 @@ inline void jumpIfTruthy(AssemblyBuilderX64& build, int ri, Label& target, Label
void jumpOnNumberCmp(AssemblyBuilderX64& build, RegisterX64 tmp, OperandX64 lhs, OperandX64 rhs, IrCondition cond, Label& label);
ConditionX64 getConditionInt(IrCondition cond);
void getTableNodeAtCachedSlot(AssemblyBuilderX64& build, RegisterX64 tmp, RegisterX64 node, RegisterX64 table, int pcpos);
void convertNumberToIndexOrJump(AssemblyBuilderX64& build, RegisterX64 tmp, RegisterX64 numd, RegisterX64 numi, Label& label);

View File

@ -149,6 +149,12 @@ void IrBuilder::buildFunctionIr(Proto* proto)
// We skip dead bytecode instructions when they appear after block was already terminated
if (!inTerminatedBlock)
{
if (interruptRequested)
{
interruptRequested = false;
inst(IrCmd::INTERRUPT, constUint(i));
}
translateInst(op, pc, i);
if (fastcallSkipTarget != -1)

View File

@ -157,12 +157,8 @@ const char* getCmdName(IrCmd cmd)
return "JUMP_IF_FALSY";
case IrCmd::JUMP_EQ_TAG:
return "JUMP_EQ_TAG";
case IrCmd::JUMP_EQ_INT:
return "JUMP_EQ_INT";
case IrCmd::JUMP_LT_INT:
return "JUMP_LT_INT";
case IrCmd::JUMP_GE_UINT:
return "JUMP_GE_UINT";
case IrCmd::JUMP_CMP_INT:
return "JUMP_CMP_INT";
case IrCmd::JUMP_EQ_POINTER:
return "JUMP_EQ_POINTER";
case IrCmd::JUMP_CMP_NUM:

View File

@ -58,6 +58,58 @@ inline ConditionA64 getConditionFP(IrCondition cond)
}
}
inline ConditionA64 getConditionInt(IrCondition cond)
{
switch (cond)
{
case IrCondition::Equal:
return ConditionA64::Equal;
case IrCondition::NotEqual:
return ConditionA64::NotEqual;
case IrCondition::Less:
return ConditionA64::Minus;
case IrCondition::NotLess:
return ConditionA64::Plus;
case IrCondition::LessEqual:
return ConditionA64::LessEqual;
case IrCondition::NotLessEqual:
return ConditionA64::Greater;
case IrCondition::Greater:
return ConditionA64::Greater;
case IrCondition::NotGreater:
return ConditionA64::LessEqual;
case IrCondition::GreaterEqual:
return ConditionA64::GreaterEqual;
case IrCondition::NotGreaterEqual:
return ConditionA64::Less;
case IrCondition::UnsignedLess:
return ConditionA64::CarryClear;
case IrCondition::UnsignedLessEqual:
return ConditionA64::UnsignedLessEqual;
case IrCondition::UnsignedGreater:
return ConditionA64::UnsignedGreater;
case IrCondition::UnsignedGreaterEqual:
return ConditionA64::CarrySet;
default:
LUAU_ASSERT(!"Unexpected condition code");
return ConditionA64::Always;
}
}
static void emitAddOffset(AssemblyBuilderA64& build, RegisterA64 dst, RegisterA64 src, size_t offset)
{
LUAU_ASSERT(dst != src);
@ -714,31 +766,25 @@ void IrLoweringA64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next)
}
break;
}
case IrCmd::JUMP_EQ_INT:
if (intOp(inst.b) == 0)
case IrCmd::JUMP_CMP_INT:
{
IrCondition cond = conditionOp(inst.c);
if (cond == IrCondition::Equal && intOp(inst.b) == 0)
{
build.cbz(regOp(inst.a), labelOp(inst.c));
build.cbz(regOp(inst.a), labelOp(inst.d));
}
else if (cond == IrCondition::NotEqual && intOp(inst.b) == 0)
{
build.cbnz(regOp(inst.a), labelOp(inst.d));
}
else
{
LUAU_ASSERT(unsigned(intOp(inst.b)) <= AssemblyBuilderA64::kMaxImmediate);
build.cmp(regOp(inst.a), uint16_t(intOp(inst.b)));
build.b(ConditionA64::Equal, labelOp(inst.c));
build.b(getConditionInt(cond), labelOp(inst.d));
}
jumpOrFallthrough(blockOp(inst.d), next);
break;
case IrCmd::JUMP_LT_INT:
LUAU_ASSERT(unsigned(intOp(inst.b)) <= AssemblyBuilderA64::kMaxImmediate);
build.cmp(regOp(inst.a), uint16_t(intOp(inst.b)));
build.b(ConditionA64::Less, labelOp(inst.c));
jumpOrFallthrough(blockOp(inst.d), next);
break;
case IrCmd::JUMP_GE_UINT:
{
LUAU_ASSERT(unsigned(intOp(inst.b)) <= AssemblyBuilderA64::kMaxImmediate);
build.cmp(regOp(inst.a), uint16_t(unsigned(intOp(inst.b))));
build.b(ConditionA64::CarrySet, labelOp(inst.c));
jumpOrFallthrough(blockOp(inst.d), next);
jumpOrFallthrough(blockOp(inst.e), next);
break;
}
case IrCmd::JUMP_EQ_POINTER:

View File

@ -655,42 +655,36 @@ void IrLoweringX64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next)
}
break;
}
case IrCmd::JUMP_EQ_INT:
if (intOp(inst.b) == 0)
case IrCmd::JUMP_CMP_INT:
{
IrCondition cond = conditionOp(inst.c);
if ((cond == IrCondition::Equal || cond == IrCondition::NotEqual) && intOp(inst.b) == 0)
{
bool invert = cond == IrCondition::NotEqual;
build.test(regOp(inst.a), regOp(inst.a));
if (isFallthroughBlock(blockOp(inst.c), next))
if (isFallthroughBlock(blockOp(inst.d), next))
{
build.jcc(ConditionX64::NotZero, labelOp(inst.d));
jumpOrFallthrough(blockOp(inst.c), next);
build.jcc(invert ? ConditionX64::Zero : ConditionX64::NotZero, labelOp(inst.e));
jumpOrFallthrough(blockOp(inst.d), next);
}
else
{
build.jcc(ConditionX64::Zero, labelOp(inst.c));
jumpOrFallthrough(blockOp(inst.d), next);
build.jcc(invert ? ConditionX64::NotZero : ConditionX64::Zero, labelOp(inst.d));
jumpOrFallthrough(blockOp(inst.e), next);
}
}
else
{
build.cmp(regOp(inst.a), intOp(inst.b));
build.jcc(ConditionX64::Equal, labelOp(inst.c));
jumpOrFallthrough(blockOp(inst.d), next);
build.jcc(getConditionInt(cond), labelOp(inst.d));
jumpOrFallthrough(blockOp(inst.e), next);
}
break;
case IrCmd::JUMP_LT_INT:
build.cmp(regOp(inst.a), intOp(inst.b));
build.jcc(ConditionX64::Less, labelOp(inst.c));
jumpOrFallthrough(blockOp(inst.d), next);
break;
case IrCmd::JUMP_GE_UINT:
build.cmp(regOp(inst.a), unsigned(intOp(inst.b)));
build.jcc(ConditionX64::AboveEqual, labelOp(inst.c));
jumpOrFallthrough(blockOp(inst.d), next);
break;
}
case IrCmd::JUMP_EQ_POINTER:
build.cmp(regOp(inst.a), regOp(inst.b));
@ -703,7 +697,6 @@ void IrLoweringX64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next)
ScopedRegX64 tmp{regs, SizeX64::xmmword};
// TODO: jumpOnNumberCmp should work on IrCondition directly
jumpOnNumberCmp(build, tmp.reg, memRegDoubleOp(inst.a), memRegDoubleOp(inst.b), cond, labelOp(inst.d));
jumpOrFallthrough(blockOp(inst.e), next);
break;

View File

@ -411,7 +411,7 @@ static BuiltinImplResult translateBuiltinBit32BinaryOp(
IrOp falsey = build.block(IrBlockKind::Internal);
IrOp truthy = build.block(IrBlockKind::Internal);
IrOp exit = build.block(IrBlockKind::Internal);
build.inst(IrCmd::JUMP_EQ_INT, res, build.constInt(0), falsey, truthy);
build.inst(IrCmd::JUMP_CMP_INT, res, build.constInt(0), build.cond(IrCondition::Equal), falsey, truthy);
build.beginBlock(falsey);
build.inst(IrCmd::STORE_INT, build.vmReg(ra), build.constInt(0));
@ -484,7 +484,7 @@ static BuiltinImplResult translateBuiltinBit32Shift(
if (!knownGoodShift)
{
IrOp block = build.block(IrBlockKind::Internal);
build.inst(IrCmd::JUMP_GE_UINT, vbi, build.constInt(32), fallback, block);
build.inst(IrCmd::JUMP_CMP_INT, vbi, build.constInt(32), build.cond(IrCondition::UnsignedGreaterEqual), fallback, block);
build.beginBlock(block);
}
@ -549,36 +549,56 @@ static BuiltinImplResult translateBuiltinBit32Extract(
IrOp vb = builtinLoadDouble(build, args);
IrOp n = build.inst(IrCmd::NUM_TO_UINT, va);
IrOp f = build.inst(IrCmd::NUM_TO_INT, vb);
IrOp value;
if (nparams == 2)
{
IrOp block = build.block(IrBlockKind::Internal);
build.inst(IrCmd::JUMP_GE_UINT, f, build.constInt(32), fallback, block);
build.beginBlock(block);
if (vb.kind == IrOpKind::Constant)
{
int f = int(build.function.doubleOp(vb));
// TODO: this can be optimized using a bit-select instruction (bt on x86)
IrOp shift = build.inst(IrCmd::BITRSHIFT_UINT, n, f);
value = build.inst(IrCmd::BITAND_UINT, shift, build.constInt(1));
if (unsigned(f) >= 32)
build.inst(IrCmd::JUMP, fallback);
// TODO: this pair can be optimized using a bit-select instruction (bt on x86)
if (f)
value = build.inst(IrCmd::BITRSHIFT_UINT, n, build.constInt(f));
if ((f + 1) < 32)
value = build.inst(IrCmd::BITAND_UINT, value, build.constInt(1));
}
else
{
IrOp f = build.inst(IrCmd::NUM_TO_INT, vb);
IrOp block = build.block(IrBlockKind::Internal);
build.inst(IrCmd::JUMP_CMP_INT, f, build.constInt(32), build.cond(IrCondition::UnsignedGreaterEqual), fallback, block);
build.beginBlock(block);
// TODO: this pair can be optimized using a bit-select instruction (bt on x86)
IrOp shift = build.inst(IrCmd::BITRSHIFT_UINT, n, f);
value = build.inst(IrCmd::BITAND_UINT, shift, build.constInt(1));
}
}
else
{
IrOp f = build.inst(IrCmd::NUM_TO_INT, vb);
builtinCheckDouble(build, build.vmReg(args.index + 1), pcpos);
IrOp vc = builtinLoadDouble(build, build.vmReg(args.index + 1));
IrOp w = build.inst(IrCmd::NUM_TO_INT, vc);
IrOp block1 = build.block(IrBlockKind::Internal);
build.inst(IrCmd::JUMP_LT_INT, f, build.constInt(0), fallback, block1);
build.inst(IrCmd::JUMP_CMP_INT, f, build.constInt(0), build.cond(IrCondition::Less), fallback, block1);
build.beginBlock(block1);
IrOp block2 = build.block(IrBlockKind::Internal);
build.inst(IrCmd::JUMP_LT_INT, w, build.constInt(1), fallback, block2);
build.inst(IrCmd::JUMP_CMP_INT, w, build.constInt(1), build.cond(IrCondition::Less), fallback, block2);
build.beginBlock(block2);
IrOp block3 = build.block(IrBlockKind::Internal);
IrOp fw = build.inst(IrCmd::ADD_INT, f, w);
build.inst(IrCmd::JUMP_LT_INT, fw, build.constInt(33), block3, fallback);
build.inst(IrCmd::JUMP_CMP_INT, fw, build.constInt(33), build.cond(IrCondition::Less), block3, fallback);
build.beginBlock(block3);
IrOp shift = build.inst(IrCmd::BITLSHIFT_UINT, build.constInt(0xfffffffe), build.inst(IrCmd::SUB_INT, w, build.constInt(1)));
@ -615,10 +635,15 @@ static BuiltinImplResult translateBuiltinBit32ExtractK(
uint32_t m = ~(0xfffffffeu << w1);
IrOp nf = build.inst(IrCmd::BITRSHIFT_UINT, n, build.constInt(f));
IrOp and_ = build.inst(IrCmd::BITAND_UINT, nf, build.constInt(m));
IrOp result = n;
IrOp value = build.inst(IrCmd::UINT_TO_NUM, and_);
if (f)
result = build.inst(IrCmd::BITRSHIFT_UINT, result, build.constInt(f));
if ((f + w1 + 1) < 32)
result = build.inst(IrCmd::BITAND_UINT, result, build.constInt(m));
IrOp value = build.inst(IrCmd::UINT_TO_NUM, result);
build.inst(IrCmd::STORE_DOUBLE, build.vmReg(ra), value);
if (ra != arg)
@ -673,7 +698,7 @@ static BuiltinImplResult translateBuiltinBit32Replace(
if (nparams == 3)
{
IrOp block = build.block(IrBlockKind::Internal);
build.inst(IrCmd::JUMP_GE_UINT, f, build.constInt(32), fallback, block);
build.inst(IrCmd::JUMP_CMP_INT, f, build.constInt(32), build.cond(IrCondition::UnsignedGreaterEqual), fallback, block);
build.beginBlock(block);
// TODO: this can be optimized using a bit-select instruction (btr on x86)
@ -694,16 +719,16 @@ static BuiltinImplResult translateBuiltinBit32Replace(
IrOp w = build.inst(IrCmd::NUM_TO_INT, vd);
IrOp block1 = build.block(IrBlockKind::Internal);
build.inst(IrCmd::JUMP_LT_INT, f, build.constInt(0), fallback, block1);
build.inst(IrCmd::JUMP_CMP_INT, f, build.constInt(0), build.cond(IrCondition::Less), fallback, block1);
build.beginBlock(block1);
IrOp block2 = build.block(IrBlockKind::Internal);
build.inst(IrCmd::JUMP_LT_INT, w, build.constInt(1), fallback, block2);
build.inst(IrCmd::JUMP_CMP_INT, w, build.constInt(1), build.cond(IrCondition::Less), fallback, block2);
build.beginBlock(block2);
IrOp block3 = build.block(IrBlockKind::Internal);
IrOp fw = build.inst(IrCmd::ADD_INT, f, w);
build.inst(IrCmd::JUMP_LT_INT, fw, build.constInt(33), block3, fallback);
build.inst(IrCmd::JUMP_CMP_INT, fw, build.constInt(33), build.cond(IrCondition::Less), block3, fallback);
build.beginBlock(block3);
IrOp shift1 = build.inst(IrCmd::BITLSHIFT_UINT, build.constInt(0xfffffffe), build.inst(IrCmd::SUB_INT, w, build.constInt(1)));

View File

@ -12,6 +12,8 @@
#include "lstate.h"
#include "ltm.h"
LUAU_FASTFLAGVARIABLE(LuauImproveForN, false)
namespace Luau
{
namespace CodeGen
@ -170,7 +172,7 @@ void translateInstJumpIfEq(IrBuilder& build, const Instruction* pc, int pcpos, b
build.inst(IrCmd::SET_SAVEDPC, build.constUint(pcpos + 1));
IrOp result = build.inst(IrCmd::CMP_ANY, build.vmReg(ra), build.vmReg(rb), build.cond(IrCondition::Equal));
build.inst(IrCmd::JUMP_EQ_INT, result, build.constInt(0), not_ ? target : next, not_ ? next : target);
build.inst(IrCmd::JUMP_CMP_INT, result, build.constInt(0), build.cond(IrCondition::Equal), not_ ? target : next, not_ ? next : target);
build.beginBlock(next);
}
@ -218,7 +220,7 @@ void translateInstJumpIfCond(IrBuilder& build, const Instruction* pc, int pcpos,
}
IrOp result = build.inst(IrCmd::CMP_ANY, build.vmReg(ra), build.vmReg(rb), build.cond(cond));
build.inst(IrCmd::JUMP_EQ_INT, result, build.constInt(0), reverse ? target : next, reverse ? next : target);
build.inst(IrCmd::JUMP_CMP_INT, result, build.constInt(0), build.cond(IrCondition::Equal), reverse ? target : next, reverse ? next : target);
build.beginBlock(next);
}
@ -262,7 +264,7 @@ void translateInstJumpxEqB(IrBuilder& build, const Instruction* pc, int pcpos)
build.beginBlock(checkValue);
IrOp va = build.inst(IrCmd::LOAD_INT, build.vmReg(ra));
build.inst(IrCmd::JUMP_EQ_INT, va, build.constInt(aux & 0x1), not_ ? next : target, not_ ? target : next);
build.inst(IrCmd::JUMP_CMP_INT, va, build.constInt(aux & 0x1), build.cond(IrCondition::Equal), not_ ? next : target, not_ ? target : next);
// Fallthrough in original bytecode is implicit, so we start next internal block here
if (build.isInternalBlock(next))
@ -607,6 +609,27 @@ IrOp translateFastCallN(IrBuilder& build, const Instruction* pc, int pcpos, bool
return fallback;
}
// numeric for loop always ends with the computation of step that targets ra+1
// any conditionals would result in a split basic block, so we can recover the step constants by pattern matching the IR we generated for LOADN/K
static IrOp getLoopStepK(IrBuilder& build, int ra)
{
IrBlock& active = build.function.blocks[build.activeBlockIdx];
if (active.start + 2 < build.function.instructions.size())
{
IrInst& sv = build.function.instructions[build.function.instructions.size() - 2];
IrInst& st = build.function.instructions[build.function.instructions.size() - 1];
// We currently expect to match IR generated from LOADN/LOADK so we match a particular sequence of opcodes
// In the future this can be extended to cover opposite STORE order as well as STORE_SPLIT_TVALUE
if (sv.cmd == IrCmd::STORE_DOUBLE && sv.a.kind == IrOpKind::VmReg && sv.a.index == ra + 1 && sv.b.kind == IrOpKind::Constant &&
st.cmd == IrCmd::STORE_TAG && st.a.kind == IrOpKind::VmReg && st.a.index == ra + 1 && build.function.tagOp(st.b) == LUA_TNUMBER)
return sv.b;
}
return build.undef();
}
void translateInstForNPrep(IrBuilder& build, const Instruction* pc, int pcpos)
{
int ra = LUAU_INSN_A(*pc);
@ -614,40 +637,103 @@ void translateInstForNPrep(IrBuilder& build, const Instruction* pc, int pcpos)
IrOp loopStart = build.blockAtInst(pcpos + getOpLength(LuauOpcode(LUAU_INSN_OP(*pc))));
IrOp loopExit = build.blockAtInst(getJumpTarget(*pc, pcpos));
IrOp direct = build.block(IrBlockKind::Internal);
IrOp reverse = build.block(IrBlockKind::Internal);
if (FFlag::LuauImproveForN)
{
IrOp stepK = getLoopStepK(build, ra);
build.loopStepStack.push_back(stepK);
// When loop parameters are not numbers, VM tries to perform type coercion from string and raises an exception if that fails
// Performing that fallback in native code increases code size and complicates CFG, obscuring the values when they are constant
// To avoid that overhead for an extreemely rare case (that doesn't even typecheck), we exit to VM to handle it
IrOp tagLimit = build.inst(IrCmd::LOAD_TAG, build.vmReg(ra + 0));
build.inst(IrCmd::CHECK_TAG, tagLimit, build.constTag(LUA_TNUMBER), build.vmExit(pcpos));
IrOp tagStep = build.inst(IrCmd::LOAD_TAG, build.vmReg(ra + 1));
build.inst(IrCmd::CHECK_TAG, tagStep, build.constTag(LUA_TNUMBER), build.vmExit(pcpos));
IrOp tagIdx = build.inst(IrCmd::LOAD_TAG, build.vmReg(ra + 2));
build.inst(IrCmd::CHECK_TAG, tagIdx, build.constTag(LUA_TNUMBER), build.vmExit(pcpos));
// When loop parameters are not numbers, VM tries to perform type coercion from string and raises an exception if that fails
// Performing that fallback in native code increases code size and complicates CFG, obscuring the values when they are constant
// To avoid that overhead for an extremely rare case (that doesn't even typecheck), we exit to VM to handle it
IrOp tagLimit = build.inst(IrCmd::LOAD_TAG, build.vmReg(ra + 0));
build.inst(IrCmd::CHECK_TAG, tagLimit, build.constTag(LUA_TNUMBER), build.vmExit(pcpos));
IrOp tagIdx = build.inst(IrCmd::LOAD_TAG, build.vmReg(ra + 2));
build.inst(IrCmd::CHECK_TAG, tagIdx, build.constTag(LUA_TNUMBER), build.vmExit(pcpos));
IrOp zero = build.constDouble(0.0);
IrOp limit = build.inst(IrCmd::LOAD_DOUBLE, build.vmReg(ra + 0));
IrOp step = build.inst(IrCmd::LOAD_DOUBLE, build.vmReg(ra + 1));
IrOp idx = build.inst(IrCmd::LOAD_DOUBLE, build.vmReg(ra + 2));
IrOp limit = build.inst(IrCmd::LOAD_DOUBLE, build.vmReg(ra + 0));
IrOp idx = build.inst(IrCmd::LOAD_DOUBLE, build.vmReg(ra + 2));
// step <= 0
build.inst(IrCmd::JUMP_CMP_NUM, step, zero, build.cond(IrCondition::LessEqual), reverse, direct);
if (stepK.kind == IrOpKind::Undef)
{
IrOp tagStep = build.inst(IrCmd::LOAD_TAG, build.vmReg(ra + 1));
build.inst(IrCmd::CHECK_TAG, tagStep, build.constTag(LUA_TNUMBER), build.vmExit(pcpos));
// TODO: target branches can probably be arranged better, but we need tests for NaN behavior preservation
IrOp direct = build.block(IrBlockKind::Internal);
IrOp reverse = build.block(IrBlockKind::Internal);
// step <= 0 is false, check idx <= limit
build.beginBlock(direct);
build.inst(IrCmd::JUMP_CMP_NUM, idx, limit, build.cond(IrCondition::LessEqual), loopStart, loopExit);
IrOp zero = build.constDouble(0.0);
IrOp step = build.inst(IrCmd::LOAD_DOUBLE, build.vmReg(ra + 1));
// step <= 0 is true, check limit <= idx
build.beginBlock(reverse);
build.inst(IrCmd::JUMP_CMP_NUM, limit, idx, build.cond(IrCondition::LessEqual), loopStart, loopExit);
// step > 0
// note: equivalent to 0 < step, but lowers into one instruction on both X64 and A64
build.inst(IrCmd::JUMP_CMP_NUM, step, zero, build.cond(IrCondition::Greater), direct, reverse);
// Condition to start the loop: step > 0 ? idx <= limit : limit <= idx
// We invert the condition so that loopStart is the fallthrough (false) label
// step > 0 is false, check limit <= idx
build.beginBlock(reverse);
build.inst(IrCmd::JUMP_CMP_NUM, limit, idx, build.cond(IrCondition::NotLessEqual), loopExit, loopStart);
// step > 0 is true, check idx <= limit
build.beginBlock(direct);
build.inst(IrCmd::JUMP_CMP_NUM, idx, limit, build.cond(IrCondition::NotLessEqual), loopExit, loopStart);
}
else
{
double stepN = build.function.doubleOp(stepK);
// Condition to start the loop: step > 0 ? idx <= limit : limit <= idx
// We invert the condition so that loopStart is the fallthrough (false) label
if (stepN > 0)
build.inst(IrCmd::JUMP_CMP_NUM, idx, limit, build.cond(IrCondition::NotLessEqual), loopExit, loopStart);
else
build.inst(IrCmd::JUMP_CMP_NUM, limit, idx, build.cond(IrCondition::NotLessEqual), loopExit, loopStart);
}
}
else
{
IrOp direct = build.block(IrBlockKind::Internal);
IrOp reverse = build.block(IrBlockKind::Internal);
// When loop parameters are not numbers, VM tries to perform type coercion from string and raises an exception if that fails
// Performing that fallback in native code increases code size and complicates CFG, obscuring the values when they are constant
// To avoid that overhead for an extreemely rare case (that doesn't even typecheck), we exit to VM to handle it
IrOp tagLimit = build.inst(IrCmd::LOAD_TAG, build.vmReg(ra + 0));
build.inst(IrCmd::CHECK_TAG, tagLimit, build.constTag(LUA_TNUMBER), build.vmExit(pcpos));
IrOp tagStep = build.inst(IrCmd::LOAD_TAG, build.vmReg(ra + 1));
build.inst(IrCmd::CHECK_TAG, tagStep, build.constTag(LUA_TNUMBER), build.vmExit(pcpos));
IrOp tagIdx = build.inst(IrCmd::LOAD_TAG, build.vmReg(ra + 2));
build.inst(IrCmd::CHECK_TAG, tagIdx, build.constTag(LUA_TNUMBER), build.vmExit(pcpos));
IrOp zero = build.constDouble(0.0);
IrOp limit = build.inst(IrCmd::LOAD_DOUBLE, build.vmReg(ra + 0));
IrOp step = build.inst(IrCmd::LOAD_DOUBLE, build.vmReg(ra + 1));
IrOp idx = build.inst(IrCmd::LOAD_DOUBLE, build.vmReg(ra + 2));
// step <= 0
build.inst(IrCmd::JUMP_CMP_NUM, step, zero, build.cond(IrCondition::LessEqual), reverse, direct);
// TODO: target branches can probably be arranged better, but we need tests for NaN behavior preservation
// step <= 0 is false, check idx <= limit
build.beginBlock(direct);
build.inst(IrCmd::JUMP_CMP_NUM, idx, limit, build.cond(IrCondition::LessEqual), loopStart, loopExit);
// step <= 0 is true, check limit <= idx
build.beginBlock(reverse);
build.inst(IrCmd::JUMP_CMP_NUM, limit, idx, build.cond(IrCondition::LessEqual), loopStart, loopExit);
}
// Fallthrough in original bytecode is implicit, so we start next internal block here
if (build.isInternalBlock(loopStart))
build.beginBlock(loopStart);
// VM places interrupt in FORNLOOP, but that creates a likely spill point for short loops that use loop index as INTERRUPT always spills
// We place the interrupt at the beginning of the loop body instead; VM uses FORNLOOP because it doesn't want to waste an extra instruction.
// Because loop block may not have been started yet (as it's started when lowering the first instruction!), we need to defer INTERRUPT placement.
if (FFlag::LuauImproveForN)
build.interruptRequested = true;
}
void translateInstForNLoop(IrBuilder& build, const Instruction* pc, int pcpos)
@ -657,29 +743,76 @@ void translateInstForNLoop(IrBuilder& build, const Instruction* pc, int pcpos)
IrOp loopRepeat = build.blockAtInst(getJumpTarget(*pc, pcpos));
IrOp loopExit = build.blockAtInst(pcpos + getOpLength(LuauOpcode(LUAU_INSN_OP(*pc))));
build.inst(IrCmd::INTERRUPT, build.constUint(pcpos));
if (FFlag::LuauImproveForN)
{
LUAU_ASSERT(!build.loopStepStack.empty());
IrOp stepK = build.loopStepStack.back();
build.loopStepStack.pop_back();
IrOp zero = build.constDouble(0.0);
IrOp limit = build.inst(IrCmd::LOAD_DOUBLE, build.vmReg(ra + 0));
IrOp step = build.inst(IrCmd::LOAD_DOUBLE, build.vmReg(ra + 1));
IrOp zero = build.constDouble(0.0);
IrOp limit = build.inst(IrCmd::LOAD_DOUBLE, build.vmReg(ra + 0));
IrOp step = stepK.kind == IrOpKind::Undef ? build.inst(IrCmd::LOAD_DOUBLE, build.vmReg(ra + 1)) : stepK;
IrOp idx = build.inst(IrCmd::LOAD_DOUBLE, build.vmReg(ra + 2));
idx = build.inst(IrCmd::ADD_NUM, idx, step);
build.inst(IrCmd::STORE_DOUBLE, build.vmReg(ra + 2), idx);
IrOp idx = build.inst(IrCmd::LOAD_DOUBLE, build.vmReg(ra + 2));
idx = build.inst(IrCmd::ADD_NUM, idx, step);
build.inst(IrCmd::STORE_DOUBLE, build.vmReg(ra + 2), idx);
IrOp direct = build.block(IrBlockKind::Internal);
IrOp reverse = build.block(IrBlockKind::Internal);
if (stepK.kind == IrOpKind::Undef)
{
IrOp direct = build.block(IrBlockKind::Internal);
IrOp reverse = build.block(IrBlockKind::Internal);
// step <= 0
build.inst(IrCmd::JUMP_CMP_NUM, step, zero, build.cond(IrCondition::LessEqual), reverse, direct);
// step > 0
// note: equivalent to 0 < step, but lowers into one instruction on both X64 and A64
build.inst(IrCmd::JUMP_CMP_NUM, step, zero, build.cond(IrCondition::Greater), direct, reverse);
// step <= 0 is false, check idx <= limit
build.beginBlock(direct);
build.inst(IrCmd::JUMP_CMP_NUM, idx, limit, build.cond(IrCondition::LessEqual), loopRepeat, loopExit);
// Condition to continue the loop: step > 0 ? idx <= limit : limit <= idx
// step <= 0 is true, check limit <= idx
build.beginBlock(reverse);
build.inst(IrCmd::JUMP_CMP_NUM, limit, idx, build.cond(IrCondition::LessEqual), loopRepeat, loopExit);
// step > 0 is false, check limit <= idx
build.beginBlock(reverse);
build.inst(IrCmd::JUMP_CMP_NUM, limit, idx, build.cond(IrCondition::LessEqual), loopRepeat, loopExit);
// step > 0 is true, check idx <= limit
build.beginBlock(direct);
build.inst(IrCmd::JUMP_CMP_NUM, idx, limit, build.cond(IrCondition::LessEqual), loopRepeat, loopExit);
}
else
{
double stepN = build.function.doubleOp(stepK);
// Condition to continue the loop: step > 0 ? idx <= limit : limit <= idx
if (stepN > 0)
build.inst(IrCmd::JUMP_CMP_NUM, idx, limit, build.cond(IrCondition::LessEqual), loopRepeat, loopExit);
else
build.inst(IrCmd::JUMP_CMP_NUM, limit, idx, build.cond(IrCondition::LessEqual), loopRepeat, loopExit);
}
}
else
{
build.inst(IrCmd::INTERRUPT, build.constUint(pcpos));
IrOp zero = build.constDouble(0.0);
IrOp limit = build.inst(IrCmd::LOAD_DOUBLE, build.vmReg(ra + 0));
IrOp step = build.inst(IrCmd::LOAD_DOUBLE, build.vmReg(ra + 1));
IrOp idx = build.inst(IrCmd::LOAD_DOUBLE, build.vmReg(ra + 2));
idx = build.inst(IrCmd::ADD_NUM, idx, step);
build.inst(IrCmd::STORE_DOUBLE, build.vmReg(ra + 2), idx);
IrOp direct = build.block(IrBlockKind::Internal);
IrOp reverse = build.block(IrBlockKind::Internal);
// step <= 0
build.inst(IrCmd::JUMP_CMP_NUM, step, zero, build.cond(IrCondition::LessEqual), reverse, direct);
// step <= 0 is false, check idx <= limit
build.beginBlock(direct);
build.inst(IrCmd::JUMP_CMP_NUM, idx, limit, build.cond(IrCondition::LessEqual), loopRepeat, loopExit);
// step <= 0 is true, check limit <= idx
build.beginBlock(reverse);
build.inst(IrCmd::JUMP_CMP_NUM, limit, idx, build.cond(IrCondition::LessEqual), loopRepeat, loopExit);
}
// Fallthrough in original bytecode is implicit, so we start next internal block here
if (build.isInternalBlock(loopExit))

View File

@ -72,9 +72,7 @@ IrValueKind getCmdValueKind(IrCmd cmd)
case IrCmd::JUMP_IF_TRUTHY:
case IrCmd::JUMP_IF_FALSY:
case IrCmd::JUMP_EQ_TAG:
case IrCmd::JUMP_EQ_INT:
case IrCmd::JUMP_LT_INT:
case IrCmd::JUMP_GE_UINT:
case IrCmd::JUMP_CMP_INT:
case IrCmd::JUMP_EQ_POINTER:
case IrCmd::JUMP_CMP_NUM:
case IrCmd::JUMP_SLOT_MATCH:
@ -422,6 +420,45 @@ bool compare(double a, double b, IrCondition cond)
return false;
}
bool compare(int a, int b, IrCondition cond)
{
switch (cond)
{
case IrCondition::Equal:
return a == b;
case IrCondition::NotEqual:
return a != b;
case IrCondition::Less:
return a < b;
case IrCondition::NotLess:
return !(a < b);
case IrCondition::LessEqual:
return a <= b;
case IrCondition::NotLessEqual:
return !(a <= b);
case IrCondition::Greater:
return a > b;
case IrCondition::NotGreater:
return !(a > b);
case IrCondition::GreaterEqual:
return a >= b;
case IrCondition::NotGreaterEqual:
return !(a >= b);
case IrCondition::UnsignedLess:
return unsigned(a) < unsigned(b);
case IrCondition::UnsignedLessEqual:
return unsigned(a) <= unsigned(b);
case IrCondition::UnsignedGreater:
return unsigned(a) > unsigned(b);
case IrCondition::UnsignedGreaterEqual:
return unsigned(a) >= unsigned(b);
default:
LUAU_ASSERT(!"Unsupported condition");
}
return false;
}
void foldConstants(IrBuilder& build, IrFunction& function, IrBlock& block, uint32_t index)
{
IrInst& inst = function.instructions[index];
@ -540,31 +577,13 @@ void foldConstants(IrBuilder& build, IrFunction& function, IrBlock& block, uint3
replace(function, block, index, {IrCmd::JUMP, inst.d});
}
break;
case IrCmd::JUMP_EQ_INT:
case IrCmd::JUMP_CMP_INT:
if (inst.a.kind == IrOpKind::Constant && inst.b.kind == IrOpKind::Constant)
{
if (function.intOp(inst.a) == function.intOp(inst.b))
replace(function, block, index, {IrCmd::JUMP, inst.c});
else
if (compare(function.intOp(inst.a), function.intOp(inst.b), conditionOp(inst.c)))
replace(function, block, index, {IrCmd::JUMP, inst.d});
}
break;
case IrCmd::JUMP_LT_INT:
if (inst.a.kind == IrOpKind::Constant && inst.b.kind == IrOpKind::Constant)
{
if (function.intOp(inst.a) < function.intOp(inst.b))
replace(function, block, index, {IrCmd::JUMP, inst.c});
else
replace(function, block, index, {IrCmd::JUMP, inst.d});
}
break;
case IrCmd::JUMP_GE_UINT:
if (inst.a.kind == IrOpKind::Constant && inst.b.kind == IrOpKind::Constant)
{
if (unsigned(function.intOp(inst.a)) >= unsigned(function.intOp(inst.b)))
replace(function, block, index, {IrCmd::JUMP, inst.c});
else
replace(function, block, index, {IrCmd::JUMP, inst.d});
replace(function, block, index, {IrCmd::JUMP, inst.e});
}
break;
case IrCmd::JUMP_CMP_NUM:

View File

@ -17,6 +17,7 @@ LUAU_FASTINTVARIABLE(LuauCodeGenReuseSlotLimit, 64)
LUAU_FASTFLAGVARIABLE(DebugLuauAbortingChecks, false)
LUAU_FASTFLAGVARIABLE(LuauReuseHashSlots2, false)
LUAU_FASTFLAGVARIABLE(LuauKeepVmapLinear, false)
LUAU_FASTFLAGVARIABLE(LuauMergeTagLoads, false)
namespace Luau
{
@ -502,9 +503,16 @@ static void constPropInInst(ConstPropState& state, IrBuilder& build, IrFunction&
{
case IrCmd::LOAD_TAG:
if (uint8_t tag = state.tryGetTag(inst.a); tag != 0xff)
{
substitute(function, inst, build.constTag(tag));
}
else if (inst.a.kind == IrOpKind::VmReg)
state.createRegLink(index, inst.a);
{
if (FFlag::LuauMergeTagLoads)
state.substituteOrRecordVmRegLoad(inst);
else
state.createRegLink(index, inst.a);
}
break;
case IrCmd::LOAD_POINTER:
if (inst.a.kind == IrOpKind::VmReg)
@ -716,44 +724,20 @@ static void constPropInInst(ConstPropState& state, IrBuilder& build, IrFunction&
else
replace(function, block, index, {IrCmd::JUMP, inst.d});
}
else if (FFlag::LuauMergeTagLoads && inst.a == inst.b)
{
replace(function, block, index, {IrCmd::JUMP, inst.c});
}
break;
}
case IrCmd::JUMP_EQ_INT:
case IrCmd::JUMP_CMP_INT:
{
std::optional<int> valueA = function.asIntOp(inst.a.kind == IrOpKind::Constant ? inst.a : state.tryGetValue(inst.a));
std::optional<int> valueB = function.asIntOp(inst.b.kind == IrOpKind::Constant ? inst.b : state.tryGetValue(inst.b));
if (valueA && valueB)
{
if (*valueA == *valueB)
replace(function, block, index, {IrCmd::JUMP, inst.c});
else
replace(function, block, index, {IrCmd::JUMP, inst.d});
}
break;
}
case IrCmd::JUMP_LT_INT:
{
std::optional<int> valueA = function.asIntOp(inst.a.kind == IrOpKind::Constant ? inst.a : state.tryGetValue(inst.a));
std::optional<int> valueB = function.asIntOp(inst.b.kind == IrOpKind::Constant ? inst.b : state.tryGetValue(inst.b));
if (valueA && valueB)
{
if (*valueA < *valueB)
replace(function, block, index, {IrCmd::JUMP, inst.c});
else
replace(function, block, index, {IrCmd::JUMP, inst.d});
}
break;
}
case IrCmd::JUMP_GE_UINT:
{
std::optional<unsigned> valueA = function.asUintOp(inst.a.kind == IrOpKind::Constant ? inst.a : state.tryGetValue(inst.a));
std::optional<unsigned> valueB = function.asUintOp(inst.b.kind == IrOpKind::Constant ? inst.b : state.tryGetValue(inst.b));
if (valueA && valueB)
{
if (*valueA >= *valueB)
if (compare(*valueA, *valueB, conditionOp(inst.c)))
replace(function, block, index, {IrCmd::JUMP, inst.c});
else
replace(function, block, index, {IrCmd::JUMP, inst.d});

View File

@ -167,6 +167,7 @@ target_sources(Luau.Analysis PRIVATE
Analysis/include/Luau/Error.h
Analysis/include/Luau/FileResolver.h
Analysis/include/Luau/Frontend.h
Analysis/include/Luau/GlobalTypes.h
Analysis/include/Luau/InsertionOrderedMap.h
Analysis/include/Luau/Instantiation.h
Analysis/include/Luau/IostreamHelpers.h
@ -226,6 +227,7 @@ target_sources(Luau.Analysis PRIVATE
Analysis/src/EmbeddedBuiltinDefinitions.cpp
Analysis/src/Error.cpp
Analysis/src/Frontend.cpp
Analysis/src/GlobalTypes.cpp
Analysis/src/Instantiation.cpp
Analysis/src/IostreamHelpers.cpp
Analysis/src/JsonEmitter.cpp
@ -365,6 +367,8 @@ if(TARGET Luau.UnitTest)
tests/AstQueryDsl.cpp
tests/AstQueryDsl.h
tests/AstVisitor.test.cpp
tests/RegisterCallbacks.h
tests/RegisterCallbacks.cpp
tests/Autocomplete.test.cpp
tests/BuiltinDefinitions.test.cpp
tests/ClassFixture.cpp
@ -447,6 +451,8 @@ endif()
if(TARGET Luau.Conformance)
# Luau.Conformance Sources
target_sources(Luau.Conformance PRIVATE
tests/RegisterCallbacks.h
tests/RegisterCallbacks.cpp
tests/Conformance.test.cpp
tests/main.cpp)
endif()
@ -464,6 +470,8 @@ if(TARGET Luau.CLI.Test)
CLI/Profiler.cpp
CLI/Repl.cpp
tests/RegisterCallbacks.h
tests/RegisterCallbacks.cpp
tests/Repl.test.cpp
tests/main.cpp)
endif()

View File

@ -135,6 +135,8 @@
// 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 1
void (*lua_iter_call_telemetry)(lua_State* L, int gtt, int stt, int itt) = NULL;
LUAU_NOINLINE void luau_callhook(lua_State* L, lua_Hook hook, void* userdata)
{
ptrdiff_t base = savestack(L, L->base);
@ -2289,6 +2291,10 @@ reentry:
{
// table or userdata with __call, will be called during FORGLOOP
// TODO: we might be able to stop supporting this depending on whether it's used in practice
void (*telemetrycb)(lua_State* L, int gtt, int stt, int itt) = lua_iter_call_telemetry;
if (telemetrycb)
telemetrycb(L, ttype(ra), ttype(ra + 1), ttype(ra + 2));
}
else if (ttistable(ra))
{

View File

@ -15,7 +15,6 @@
LUAU_FASTFLAG(LuauTraceTypesInNonstrictMode2)
LUAU_FASTFLAG(LuauSetMetatableDoesNotTimeTravel)
LUAU_FASTFLAG(LuauAutocompleteLastTypecheck)
using namespace Luau;
@ -34,36 +33,27 @@ struct ACFixtureImpl : BaseType
AutocompleteResult autocomplete(unsigned row, unsigned column)
{
if (FFlag::LuauAutocompleteLastTypecheck)
{
FrontendOptions opts;
opts.forAutocomplete = true;
this->frontend.check("MainModule", opts);
}
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);
}
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);
}
FrontendOptions opts;
opts.forAutocomplete = true;
this->frontend.check(name, opts);
return Luau::autocomplete(this->frontend, name, pos, callback);
}
@ -3699,8 +3689,6 @@ TEST_CASE_FIXTURE(ACFixture, "string_completion_outside_quotes")
TEST_CASE_FIXTURE(ACFixture, "anonymous_autofilled_empty")
{
ScopedFastFlag flag{"LuauAnonymousAutofilled1", true};
check(R"(
local function foo(a: () -> ())
a()
@ -3722,8 +3710,6 @@ foo(@1)
TEST_CASE_FIXTURE(ACFixture, "anonymous_autofilled_args")
{
ScopedFastFlag flag{"LuauAnonymousAutofilled1", true};
check(R"(
local function foo(a: (number, string) -> ())
a()
@ -3745,8 +3731,6 @@ foo(@1)
TEST_CASE_FIXTURE(ACFixture, "anonymous_autofilled_args_single_return")
{
ScopedFastFlag flag{"LuauAnonymousAutofilled1", true};
check(R"(
local function foo(a: (number, string) -> (string))
a()
@ -3768,8 +3752,6 @@ foo(@1)
TEST_CASE_FIXTURE(ACFixture, "anonymous_autofilled_args_multi_return")
{
ScopedFastFlag flag{"LuauAnonymousAutofilled1", true};
check(R"(
local function foo(a: (number, string) -> (string, number))
a()
@ -3791,8 +3773,6 @@ foo(@1)
TEST_CASE_FIXTURE(ACFixture, "anonymous_autofilled__noargs_multi_return")
{
ScopedFastFlag flag{"LuauAnonymousAutofilled1", true};
check(R"(
local function foo(a: () -> (string, number))
a()
@ -3814,8 +3794,6 @@ foo(@1)
TEST_CASE_FIXTURE(ACFixture, "anonymous_autofilled__varargs_multi_return")
{
ScopedFastFlag flag{"LuauAnonymousAutofilled1", true};
check(R"(
local function foo(a: (...number) -> (string, number))
a()
@ -3837,8 +3815,6 @@ foo(@1)
TEST_CASE_FIXTURE(ACFixture, "anonymous_autofilled_multi_varargs_multi_return")
{
ScopedFastFlag flag{"LuauAnonymousAutofilled1", true};
check(R"(
local function foo(a: (string, ...number) -> (string, number))
a()
@ -3860,8 +3836,6 @@ foo(@1)
TEST_CASE_FIXTURE(ACFixture, "anonymous_autofilled_multi_varargs_varargs_return")
{
ScopedFastFlag flag{"LuauAnonymousAutofilled1", true};
check(R"(
local function foo(a: (string, ...number) -> ...number)
a()
@ -3883,8 +3857,6 @@ foo(@1)
TEST_CASE_FIXTURE(ACFixture, "anonymous_autofilled_multi_varargs_multi_varargs_return")
{
ScopedFastFlag flag{"LuauAnonymousAutofilled1", true};
check(R"(
local function foo(a: (string, ...number) -> (boolean, ...number))
a()
@ -3906,8 +3878,6 @@ foo(@1)
TEST_CASE_FIXTURE(ACFixture, "anonymous_autofilled_named_args")
{
ScopedFastFlag flag{"LuauAnonymousAutofilled1", true};
check(R"(
local function foo(a: (foo: number, bar: string) -> (string, number))
a()
@ -3929,8 +3899,6 @@ foo(@1)
TEST_CASE_FIXTURE(ACFixture, "anonymous_autofilled_partially_args")
{
ScopedFastFlag flag{"LuauAnonymousAutofilled1", true};
check(R"(
local function foo(a: (number, bar: string) -> (string, number))
a()
@ -3952,8 +3920,6 @@ foo(@1)
TEST_CASE_FIXTURE(ACFixture, "anonymous_autofilled_partially_args_last")
{
ScopedFastFlag flag{"LuauAnonymousAutofilled1", true};
check(R"(
local function foo(a: (foo: number, string) -> (string, number))
a()
@ -3975,8 +3941,6 @@ foo(@1)
TEST_CASE_FIXTURE(ACFixture, "anonymous_autofilled_typeof_args")
{
ScopedFastFlag flag{"LuauAnonymousAutofilled1", true};
check(R"(
local t = { a = 1, b = 2 }
@ -4000,8 +3964,6 @@ foo(@1)
TEST_CASE_FIXTURE(ACFixture, "anonymous_autofilled_table_literal_args")
{
ScopedFastFlag flag{"LuauAnonymousAutofilled1", true};
check(R"(
local function foo(a: (tbl: { x: number, y: number }) -> number) return a({x=2, y = 3}) end
foo(@1)
@ -4020,8 +3982,6 @@ foo(@1)
TEST_CASE_FIXTURE(ACFixture, "anonymous_autofilled_typeof_returns")
{
ScopedFastFlag flag{"LuauAnonymousAutofilled1", true};
check(R"(
local t = { a = 1, b = 2 }
@ -4045,8 +4005,6 @@ foo(@1)
TEST_CASE_FIXTURE(ACFixture, "anonymous_autofilled_table_literal_args")
{
ScopedFastFlag flag{"LuauAnonymousAutofilled1", true};
check(R"(
local function foo(a: () -> { x: number, y: number }) return {x=2, y = 3} end
foo(@1)
@ -4065,8 +4023,6 @@ foo(@1)
TEST_CASE_FIXTURE(ACFixture, "anonymous_autofilled_typeof_vararg")
{
ScopedFastFlag flag{"LuauAnonymousAutofilled1", true};
check(R"(
local t = { a = 1, b = 2 }
@ -4090,8 +4046,6 @@ foo(@1)
TEST_CASE_FIXTURE(ACFixture, "anonymous_autofilled_generic_type_pack_vararg")
{
ScopedFastFlag flag{"LuauAnonymousAutofilled1", true};
check(R"(
local function foo<A>(a: (...A) -> number, ...: A)
return a(...)
@ -4113,8 +4067,6 @@ foo(@1)
TEST_CASE_FIXTURE(ACFixture, "anonymous_autofilled_generic_on_argument_type_pack_vararg")
{
ScopedFastFlag flag{"LuauAnonymousAutofilled1", true};
check(R"(
local function foo(a: <T...>(...: T...) -> number)
return a(4, 5, 6)

View File

@ -282,6 +282,8 @@ TEST_CASE("Assert")
TEST_CASE("Basic")
{
ScopedFastFlag sffs{"LuauFloorDivision", true};
ScopedFastFlag sfff{"LuauImproveForN", true};
runConformance("basic.lua");
}

View File

@ -9,6 +9,7 @@
#include "Luau/Parser.h"
#include "Luau/Type.h"
#include "Luau/TypeAttach.h"
#include "Luau/TypeInfer.h"
#include "Luau/Transpiler.h"
#include "doctest.h"
@ -144,8 +145,6 @@ Fixture::Fixture(bool freeze, bool prepareAutocomplete)
configResolver.defaultConfig.enabledLint.warningMask = ~0ull;
configResolver.defaultConfig.parseOptions.captureComments = true;
registerBuiltinTypes(frontend.globals);
Luau::freeze(frontend.globals.globalTypes);
Luau::freeze(frontend.globalsForAutocomplete.globalTypes);

View File

@ -1222,4 +1222,28 @@ TEST_CASE_FIXTURE(FrontendFixture, "parse_only")
CHECK_EQ("Type 'string' could not be converted into 'number'", toString(result.errors[0]));
}
TEST_CASE_FIXTURE(FrontendFixture, "markdirty_early_return")
{
ScopedFastFlag fflag("CorrectEarlyReturnInMarkDirty", true);
constexpr char moduleName[] = "game/Gui/Modules/A";
fileResolver.source[moduleName] = R"(
return 1
)";
{
std::vector<ModuleName> markedDirty;
frontend.markDirty(moduleName, &markedDirty);
CHECK(markedDirty.empty());
}
frontend.parse(moduleName);
{
std::vector<ModuleName> markedDirty;
frontend.markDirty(moduleName, &markedDirty);
CHECK(!markedDirty.empty());
}
}
TEST_SUITE_END();

View File

@ -621,11 +621,11 @@ TEST_CASE_FIXTURE(IrBuilderFixture, "ControlFlowEq")
});
withTwoBlocks([this](IrOp a, IrOp b) {
build.inst(IrCmd::JUMP_EQ_INT, build.constInt(0), build.constInt(0), a, b);
build.inst(IrCmd::JUMP_CMP_INT, build.constInt(0), build.constInt(0), build.cond(IrCondition::Equal), a, b);
});
withTwoBlocks([this](IrOp a, IrOp b) {
build.inst(IrCmd::JUMP_EQ_INT, build.constInt(0), build.constInt(1), a, b);
build.inst(IrCmd::JUMP_CMP_INT, build.constInt(0), build.constInt(1), build.cond(IrCondition::Equal), a, b);
});
updateUseCounts(build.function);
@ -1359,7 +1359,7 @@ TEST_CASE_FIXTURE(IrBuilderFixture, "IntEqRemoval")
build.beginBlock(block);
build.inst(IrCmd::STORE_INT, build.vmReg(1), build.constInt(5));
IrOp value = build.inst(IrCmd::LOAD_INT, build.vmReg(1));
build.inst(IrCmd::JUMP_EQ_INT, value, build.constInt(5), trueBlock, falseBlock);
build.inst(IrCmd::JUMP_CMP_INT, value, build.constInt(5), build.cond(IrCondition::Equal), trueBlock, falseBlock);
build.beginBlock(trueBlock);
build.inst(IrCmd::RETURN, build.constUint(1));
@ -1556,7 +1556,7 @@ TEST_CASE_FIXTURE(IrBuilderFixture, "RecursiveSccUseRemoval2")
IrOp repeat = build.block(IrBlockKind::Internal);
build.beginBlock(entry);
build.inst(IrCmd::JUMP_EQ_INT, build.constInt(0), build.constInt(1), block, exit1);
build.inst(IrCmd::JUMP_CMP_INT, build.constInt(0), build.constInt(1), build.cond(IrCondition::Equal), block, exit1);
build.beginBlock(exit1);
build.inst(IrCmd::RETURN, build.vmReg(0), build.constInt(0));
@ -2785,4 +2785,37 @@ bb_0:
)");
}
TEST_CASE_FIXTURE(IrBuilderFixture, "TagSelfEqualityCheckRemoval")
{
ScopedFastFlag luauMergeTagLoads{"LuauMergeTagLoads", true};
IrOp entry = build.block(IrBlockKind::Internal);
IrOp trueBlock = build.block(IrBlockKind::Internal);
IrOp falseBlock = build.block(IrBlockKind::Internal);
build.beginBlock(entry);
IrOp tag1 = build.inst(IrCmd::LOAD_TAG, build.vmReg(0));
IrOp tag2 = build.inst(IrCmd::LOAD_TAG, build.vmReg(0));
build.inst(IrCmd::JUMP_EQ_TAG, tag1, tag2, trueBlock, falseBlock);
build.beginBlock(trueBlock);
build.inst(IrCmd::RETURN, build.constUint(1));
build.beginBlock(falseBlock);
build.inst(IrCmd::RETURN, build.constUint(2));
updateUseCounts(build.function);
constPropInBlockChains(build, true);
CHECK("\n" + toString(build.function, /* includeUseInfo */ false) == R"(
bb_0:
JUMP bb_1
bb_1:
RETURN 1u
)");
}
TEST_SUITE_END();

View File

@ -0,0 +1,20 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#include "RegisterCallbacks.h"
namespace Luau
{
std::unordered_set<RegisterCallback>& getRegisterCallbacks()
{
static std::unordered_set<RegisterCallback> cbs;
return cbs;
}
int addTestCallback(RegisterCallback cb)
{
getRegisterCallbacks().insert(cb);
return 0;
}
} // namespace Luau

22
tests/RegisterCallbacks.h Normal file
View File

@ -0,0 +1,22 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#pragma once
#include <unordered_set>
#include <string>
namespace Luau
{
using RegisterCallback = void (*)();
/// Gets a set of callbacks to run immediately before running tests, intended
/// for registering new tests at runtime.
std::unordered_set<RegisterCallback>& getRegisterCallbacks();
/// Adds a new callback to be ran immediately before running tests.
///
/// @param cb the callback to add.
/// @returns a dummy integer to satisfy a doctest internal contract.
int addTestCallback(RegisterCallback cb);
} // namespace Luau

View File

@ -2,7 +2,9 @@
#include "doctest.h"
#include "Fixture.h"
#include "RegisterCallbacks.h"
#include "Luau/Normalize.h"
#include "Luau/Subtyping.h"
#include "Luau/TypePack.h"
@ -344,14 +346,72 @@ struct SubtypeFixture : Fixture
CHECK_MESSAGE(!result.isErrorSuppressing, "Expected " << leftTy << " to error-suppress " << rightTy); \
} while (0)
/// Internal macro for registering a generated test case.
///
/// @param der the name of the derived fixture struct
/// @param reg the name of the registration callback, invoked immediately before
/// tests are ran to register the test
/// @param run the name of the run callback, invoked to actually run the test case
#define TEST_REGISTER(der, reg, run) \
static inline DOCTEST_NOINLINE void run() \
{ \
der fix; \
fix.test(); \
} \
static inline DOCTEST_NOINLINE void reg() \
{ \
/* we have to mark this as `static` to ensure the memory remains alive \
for the entirety of the test process */ \
static std::string name = der().testName; \
doctest::detail::regTest(doctest::detail::TestCase(run, __FILE__, __LINE__, \
doctest_detail_test_suite_ns::getCurrentTestSuite()) /* the test case's name, determined at runtime */ \
* name.c_str() /* getCurrentTestSuite() only works at static initialization \
time due to implementation details. To ensure that test cases \
are grouped where they should be, manually override the suite \
with the test_suite decorator. */ \
* doctest::test_suite("Subtyping")); \
} \
DOCTEST_GLOBAL_NO_WARNINGS(DOCTEST_ANONYMOUS(DOCTEST_ANON_VAR_), addTestCallback(reg));
/// Internal macro for deriving a test case fixture. Roughly analogous to
/// DOCTEST_IMPLEMENT_FIXTURE.
///
/// @param op a function (or macro) to call that compares the subtype to
/// the supertype.
/// @param symbol the symbol to use in stringification
/// @param der the name of the derived fixture struct
/// @param left the subtype expression
/// @param right the supertype expression
#define TEST_DERIVE(op, symbol, der, left, right) \
namespace \
{ \
struct der : SubtypeFixture \
{ \
const TypeId subTy = (left); \
const TypeId superTy = (right); \
const std::string testName = toString(subTy) + " " symbol " " + toString(superTy); \
inline DOCTEST_NOINLINE void test() \
{ \
op(subTy, superTy); \
} \
}; \
TEST_REGISTER(der, DOCTEST_ANONYMOUS(DOCTEST_ANON_FUNC_), DOCTEST_ANONYMOUS(DOCTEST_ANON_FUNC_)); \
}
/// Generates a test that checks if a type is a subtype of another.
#define TEST_IS_SUBTYPE(left, right) TEST_DERIVE(CHECK_IS_SUBTYPE, "<:", DOCTEST_ANONYMOUS(DOCTEST_ANON_CLASS_), left, right)
/// Generates a test that checks if a type is _not_ a subtype of another.
/// Uses <!: instead of </: to ensure that rotest doesn't explode when it splits
/// on / characters.
#define TEST_IS_NOT_SUBTYPE(left, right) TEST_DERIVE(CHECK_IS_NOT_SUBTYPE, "<!:", DOCTEST_ANONYMOUS(DOCTEST_ANON_CLASS_), left, right)
TEST_SUITE_BEGIN("Subtyping");
// We would like to write </: to mean "is not a subtype," but rotest does not like that at all, so we instead use <!:
TEST_CASE_FIXTURE(SubtypeFixture, "number <: any")
{
CHECK_IS_SUBTYPE(builtinTypes->numberType, builtinTypes->anyType);
}
TEST_IS_SUBTYPE(builtinTypes->numberType, builtinTypes->anyType);
TEST_IS_NOT_SUBTYPE(builtinTypes->numberType, builtinTypes->stringType);
TEST_CASE_FIXTURE(SubtypeFixture, "any <!: unknown")
{
@ -375,11 +435,6 @@ TEST_CASE_FIXTURE(SubtypeFixture, "number <: number")
CHECK_IS_SUBTYPE(builtinTypes->numberType, builtinTypes->numberType);
}
TEST_CASE_FIXTURE(SubtypeFixture, "number <!: string")
{
CHECK_IS_NOT_SUBTYPE(builtinTypes->numberType, builtinTypes->stringType);
}
TEST_CASE_FIXTURE(SubtypeFixture, "number <: number?")
{
CHECK_IS_SUBTYPE(builtinTypes->numberType, builtinTypes->optionalNumberType);
@ -895,6 +950,16 @@ TEST_CASE_FIXTURE(SubtypeFixture, "string <!: { insaneThingNoScalarHas : () -> (
CHECK_IS_NOT_SUBTYPE(builtinTypes->stringType, tableWithoutScalarProp);
}
TEST_CASE_FIXTURE(SubtypeFixture, "~fun & (string) -> number <: (string) -> number")
{
CHECK_IS_SUBTYPE(meet(negate(builtinTypes->functionType), numberToStringType), numberToStringType);
}
TEST_CASE_FIXTURE(SubtypeFixture, "(string) -> number <: ~fun & (string) -> number")
{
CHECK_IS_NOT_SUBTYPE(numberToStringType, meet(negate(builtinTypes->functionType), numberToStringType));
}
/*
* <A>(A) -> A <: <X>(X) -> X
* A can be bound to X.

View File

@ -44,25 +44,34 @@ TEST_SUITE_BEGIN("ToDot");
TEST_CASE_FIXTURE(Fixture, "primitive")
{
CheckResult result = check(R"(
local a: nil
local b: number
local c: any
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_NE("nil", toDot(requireType("a")));
CHECK_EQ(R"(digraph graphname {
n1 [label="nil"];
})",
toDot(builtinTypes->nilType));
CHECK_EQ(R"(digraph graphname {
n1 [label="number"];
})",
toDot(requireType("b")));
toDot(builtinTypes->numberType));
CHECK_EQ(R"(digraph graphname {
n1 [label="any"];
})",
toDot(requireType("c")));
toDot(builtinTypes->anyType));
CHECK_EQ(R"(digraph graphname {
n1 [label="unknown"];
})",
toDot(builtinTypes->unknownType));
CHECK_EQ(R"(digraph graphname {
n1 [label="never"];
})",
toDot(builtinTypes->neverType));
}
TEST_CASE_FIXTURE(Fixture, "no_duplicatePrimitives")
{
ToDotOptions opts;
opts.showPointers = false;
opts.duplicatePrimitives = false;
@ -70,12 +79,22 @@ n1 [label="any"];
CHECK_EQ(R"(digraph graphname {
n1 [label="PrimitiveType number"];
})",
toDot(requireType("b"), opts));
toDot(builtinTypes->numberType, opts));
CHECK_EQ(R"(digraph graphname {
n1 [label="AnyType 1"];
})",
toDot(requireType("c"), opts));
toDot(builtinTypes->anyType, opts));
CHECK_EQ(R"(digraph graphname {
n1 [label="UnknownType 1"];
})",
toDot(builtinTypes->unknownType, opts));
CHECK_EQ(R"(digraph graphname {
n1 [label="NeverType 1"];
})",
toDot(builtinTypes->neverType, opts));
}
TEST_CASE_FIXTURE(Fixture, "bound")
@ -283,6 +302,30 @@ n1 [label="FreeType 1"];
toDot(&type, opts));
}
TEST_CASE_FIXTURE(Fixture, "free_with_constraints")
{
ScopedFastFlag sff[] = {
{"DebugLuauDeferredConstraintResolution", true},
};
Type type{TypeVariant{FreeType{nullptr, builtinTypes->numberType, builtinTypes->optionalNumberType}}};
ToDotOptions opts;
opts.showPointers = false;
CHECK_EQ(R"(digraph graphname {
n1 [label="FreeType 1"];
n1 -> n2 [label="[lowerBound]"];
n2 [label="number"];
n1 -> n3 [label="[upperBound]"];
n3 [label="UnionType 3"];
n3 -> n4;
n4 [label="number"];
n3 -> n5;
n5 [label="nil"];
})",
toDot(&type, opts));
}
TEST_CASE_FIXTURE(Fixture, "error")
{
Type type{TypeVariant{ErrorType{}}};
@ -440,4 +483,19 @@ n5 [label="SingletonType boolean: false"];
toDot(requireType("x"), opts));
}
TEST_CASE_FIXTURE(Fixture, "negation")
{
TypeArena arena;
TypeId t = arena.addType(NegationType{builtinTypes->stringType});
ToDotOptions opts;
opts.showPointers = false;
CHECK(R"(digraph graphname {
n1 [label="NegationType 1"];
n1 -> n2 [label="[negated]"];
n2 [label="string"];
})" == toDot(t, opts));
}
TEST_SUITE_END();

View File

@ -1,5 +1,7 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#include "Luau/TypeFamily.h"
#include "Luau/TxnLog.h"
#include "Luau/Type.h"
#include "Fixture.h"

View File

@ -1006,8 +1006,6 @@ end
// We would prefer this unification to be able to complete, but at least it should not crash
TEST_CASE_FIXTURE(BuiltinsFixture, "table_unification_infinite_recursion")
{
ScopedFastFlag luauTableUnifyRecursionLimit{"LuauTableUnifyRecursionLimit", true};
#if defined(_NOOPT) || defined(_DEBUG)
ScopedFastInt LuauTypeInferRecursionLimit{"LuauTypeInferRecursionLimit", 100};
#endif

View File

@ -1404,4 +1404,32 @@ TEST_CASE_FIXTURE(Fixture, "promote_tail_type_packs")
LUAU_REQUIRE_NO_ERRORS(result);
}
/*
* CLI-49876
*
* We had a bug where we would not use the correct TxnLog when evaluating a
* variadic overload. We could therefore get into a state where the TxnLog has
* logged that a generic matches to one type, but the variadic tail has already
* been bound to another type outside of that TxnLog.
*
* This caused type checking to succeed when it should have failed.
*/
TEST_CASE_FIXTURE(BuiltinsFixture, "be_sure_to_use_active_txnlog_when_evaluating_a_variadic_overload")
{
ScopedFastFlag sff{"LuauVariadicOverloadFix", true};
CheckResult result = check(R"(
local function concat<T>(target: {T}, ...: {T} | T): {T}
return (nil :: any) :: {T}
end
local res = concat({"alic"}, 1, 2)
)");
LUAU_REQUIRE_ERRORS(result);
for (const auto& e: result.errors)
CHECK(5 == e.location.begin.line);
}
TEST_SUITE_END();

View File

@ -177,6 +177,33 @@ assert((function() local a = 1 for b=1,9 do a = a * 2 if a == 128 then break els
-- make sure internal index is protected against modification
assert((function() local a = 1 for b=9,1,-2 do a = a * 2 b = nil end return a end)() == 32)
-- make sure that when step is 0, we treat it as backward iteration (and as such, iterate zero times or indefinitely)
-- this is consistent with Lua 5.1; future Lua versions emit an error when step is 0; LuaJIT instead treats 0 as forward iteration
-- we repeat tests twice, with and without constant folding
local zero = tonumber("0")
assert((function() local c = 0 for i=1,10,0 do c += 1 if c > 10 then break end end return c end)() == 0)
assert((function() local c = 0 for i=10,1,0 do c += 1 if c > 10 then break end end return c end)() == 11)
assert((function() local c = 0 for i=1,10,zero do c += 1 if c > 10 then break end end return c end)() == 0)
assert((function() local c = 0 for i=10,1,zero do c += 1 if c > 10 then break end end return c end)() == 11)
-- make sure that when limit is nan, we iterate zero times (this is consistent with Lua 5.1; future Lua versions break this)
-- we repeat tests twice, with and without constant folding
local nan = tonumber("nan")
assert((function() local c = 0 for i=1,0/0 do c += 1 end return c end)() == 0)
assert((function() local c = 0 for i=1,0/0,-1 do c += 1 end return c end)() == 0)
assert((function() local c = 0 for i=1,nan do c += 1 end return c end)() == 0)
assert((function() local c = 0 for i=1,nan,-1 do c += 1 end return c end)() == 0)
-- make sure that when step is nan, we treat it as backward iteration and as such iterate once iff start<=limit
assert((function() local c = 0 for i=1,10,0/0 do c += 1 end return c end)() == 0)
assert((function() local c = 0 for i=10,1,0/0 do c += 1 end return c end)() == 1)
assert((function() local c = 0 for i=1,10,nan do c += 1 end return c end)() == 0)
assert((function() local c = 0 for i=10,1,nan do c += 1 end return c end)() == 1)
-- make sure that when index becomes nan mid-iteration, we correctly exit the loop (this is broken in Lua 5.1; future Lua versions fix this)
assert((function() local c = 0 for i=-math.huge,0,math.huge do c += 1 end return c end)() == 1)
assert((function() local c = 0 for i=math.huge,math.huge,-math.huge do c += 1 end return c end)() == 1)
-- generic for
-- ipairs
assert((function() local a = '' for k in ipairs({5, 6, 7}) do a = a .. k end return a end)() == "123")
@ -286,6 +313,10 @@ assert((function()
return result
end)() == "ArcticDunesCanyonsWaterMountainsHillsLavaflowPlainsMarsh")
-- table literals may contain duplicate fields; the language doesn't specify assignment order but we currently assign left to right
assert((function() local t = {data = 4, data = nil, data = 42} return t.data end)() == 42)
assert((function() local t = {data = 4, data = nil, data = 42, data = nil} return t.data end)() == nil)
-- multiple returns
-- local=
assert((function() function foo() return 2, 3, 4 end local a, b, c = foo() return ''..a..b..c end)() == "234")

View File

@ -189,6 +189,26 @@ do -- testing NaN
assert(a[NaN] == nil)
end
-- extra NaN tests, hidden in a function
do
function neq(a) return a ~= a end
function eq(a) return a == a end
function lt(a) return a < a end
function le(a) return a <= a end
function gt(a) return a > a end
function ge(a) return a >= a end
local NaN -- to avoid constant folding
NaN = 10e500 - 10e400
assert(neq(NaN))
assert(not eq(NaN))
assert(not lt(NaN))
assert(not le(NaN))
assert(not gt(NaN))
assert(not ge(NaN))
end
-- require "checktable"
-- stat(a)

View File

@ -6,6 +6,8 @@
#define DOCTEST_CONFIG_OPTIONS_PREFIX ""
#include "doctest.h"
#include "RegisterCallbacks.h"
#ifdef _WIN32
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
@ -327,6 +329,14 @@ int main(int argc, char** argv)
}
}
// These callbacks register unit tests that need runtime support to be
// correctly set up. Running them here means that all command line flags
// have been parsed, fast flags have been set, and we've potentially already
// exited. Once doctest::Context::run is invoked, the test list will be
// picked up from global state.
for (Luau::RegisterCallback cb : Luau::getRegisterCallbacks())
cb();
int result = context.run();
if (doctest::parseFlag(argc, argv, "--help") || doctest::parseFlag(argc, argv, "-h"))
{