Sync to upstream/release/598

This commit is contained in:
Andy Friesen 2023-10-06 10:31:16 -07:00
parent 3bfc864280
commit 22e3d1fa46
72 changed files with 1946 additions and 732 deletions

View file

@ -187,12 +187,12 @@ private:
* @param generalize If true, generalize any lambdas that are encountered. * @param generalize If true, generalize any lambdas that are encountered.
* @return the type of the expression. * @return the type of the expression.
*/ */
Inference check(const ScopePtr& scope, AstExpr* expr, ValueContext context = ValueContext::RValue, std::optional<TypeId> expectedType = {}, Inference check(
bool forceSingleton = false, bool generalize = true); const ScopePtr& scope, AstExpr* expr, std::optional<TypeId> expectedType = {}, bool forceSingleton = false, bool generalize = true);
Inference check(const ScopePtr& scope, AstExprConstantString* string, std::optional<TypeId> expectedType, bool forceSingleton); Inference check(const ScopePtr& scope, AstExprConstantString* string, std::optional<TypeId> expectedType, bool forceSingleton);
Inference check(const ScopePtr& scope, AstExprConstantBool* bool_, std::optional<TypeId> expectedType, bool forceSingleton); Inference check(const ScopePtr& scope, AstExprConstantBool* bool_, std::optional<TypeId> expectedType, bool forceSingleton);
Inference check(const ScopePtr& scope, AstExprLocal* local, ValueContext context); Inference check(const ScopePtr& scope, AstExprLocal* local);
Inference check(const ScopePtr& scope, AstExprGlobal* global); Inference check(const ScopePtr& scope, AstExprGlobal* global);
Inference check(const ScopePtr& scope, AstExprIndexName* indexName); Inference check(const ScopePtr& scope, AstExprIndexName* indexName);
Inference check(const ScopePtr& scope, AstExprIndexExpr* indexExpr); Inference check(const ScopePtr& scope, AstExprIndexExpr* indexExpr);
@ -208,6 +208,11 @@ private:
std::vector<TypeId> checkLValues(const ScopePtr& scope, AstArray<AstExpr*> exprs); std::vector<TypeId> checkLValues(const ScopePtr& scope, AstArray<AstExpr*> exprs);
TypeId checkLValue(const ScopePtr& scope, AstExpr* expr); TypeId checkLValue(const ScopePtr& scope, AstExpr* expr);
TypeId checkLValue(const ScopePtr& scope, AstExprLocal* local);
TypeId checkLValue(const ScopePtr& scope, AstExprGlobal* global);
TypeId checkLValue(const ScopePtr& scope, AstExprIndexName* indexName);
TypeId checkLValue(const ScopePtr& scope, AstExprIndexExpr* indexExpr);
TypeId updateProperty(const ScopePtr& scope, AstExpr* expr);
struct FunctionSignature struct FunctionSignature
{ {

View file

@ -52,6 +52,7 @@ struct Scope
void addBuiltinTypeBinding(const Name& name, const TypeFun& tyFun); void addBuiltinTypeBinding(const Name& name, const TypeFun& tyFun);
std::optional<TypeId> lookup(Symbol sym) const; std::optional<TypeId> lookup(Symbol sym) const;
std::optional<TypeId> lookupLValue(DefId def) const;
std::optional<TypeId> lookup(DefId def) const; std::optional<TypeId> lookup(DefId def) const;
std::optional<std::pair<Binding*, Scope*>> lookupEx(Symbol sym); std::optional<std::pair<Binding*, Scope*>> lookupEx(Symbol sym);
@ -65,7 +66,15 @@ struct Scope
std::optional<Binding> linearSearchForBinding(const std::string& name, bool traverseScopeChain = true) const; std::optional<Binding> linearSearchForBinding(const std::string& name, bool traverseScopeChain = true) const;
RefinementMap refinements; RefinementMap refinements;
DenseHashMap<const Def*, TypeId> dcrRefinements{nullptr};
// This can be viewed as the "unrefined" type of each binding.
DenseHashMap<const Def*, TypeId> lvalueTypes{nullptr};
// Luau values are routinely refined more narrowly than their actual
// inferred type through control flow statements. We retain those refined
// types here.
DenseHashMap<const Def*, TypeId> rvalueRefinements{nullptr};
void inheritRefinements(const ScopePtr& childScope); void inheritRefinements(const ScopePtr& childScope);
// For mutually recursive type aliases, it's important that // For mutually recursive type aliases, it's important that

View file

@ -355,6 +355,7 @@ struct FunctionType
// `hasNoFreeOrGenericTypes` should be true if and only if the type does not have any free or generic types present inside it. // `hasNoFreeOrGenericTypes` should be true if and only if the type does not have any free or generic types present inside it.
// this flag is used as an optimization to exit early from procedures that manipulate free or generic types. // this flag is used as an optimization to exit early from procedures that manipulate free or generic types.
bool hasNoFreeOrGenericTypes = false; bool hasNoFreeOrGenericTypes = false;
bool isCheckedFunction = false;
}; };
enum class TableState enum class TableState

View file

@ -1,9 +1,13 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details // This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#pragma once #pragma once
#include "ConstraintSolver.h"
#include "Error.h"
#include "Luau/Error.h" #include "Luau/Error.h"
#include "Luau/NotNull.h" #include "Luau/NotNull.h"
#include "Luau/Variant.h" #include "Luau/Variant.h"
#include "NotNull.h"
#include "TypeCheckLimits.h"
#include <functional> #include <functional>
#include <string> #include <string>
@ -23,6 +27,42 @@ struct BuiltinTypes;
struct TxnLog; struct TxnLog;
class Normalizer; class Normalizer;
struct TypeFamilyContext
{
NotNull<TypeArena> arena;
NotNull<BuiltinTypes> builtins;
NotNull<Scope> scope;
NotNull<Normalizer> normalizer;
NotNull<InternalErrorReporter> ice;
NotNull<TypeCheckLimits> limits;
// nullptr if the type family is being reduced outside of the constraint solver.
ConstraintSolver* solver;
TypeFamilyContext(NotNull<ConstraintSolver> cs, NotNull<Scope> scope)
: arena(cs->arena)
, builtins(cs->builtinTypes)
, scope(scope)
, normalizer(cs->normalizer)
, ice(NotNull{&cs->iceReporter})
, limits(NotNull{&cs->limits})
, solver(cs.get())
{
}
TypeFamilyContext(NotNull<TypeArena> arena, NotNull<BuiltinTypes> builtins, NotNull<Scope> scope, NotNull<Normalizer> normalizer,
NotNull<InternalErrorReporter> ice, NotNull<TypeCheckLimits> limits)
: arena(arena)
, builtins(builtins)
, scope(scope)
, normalizer(normalizer)
, ice(ice)
, limits(limits)
, solver(nullptr)
{
}
};
/// Represents a reduction result, which may have successfully reduced the type, /// Represents a reduction result, which may have successfully reduced the type,
/// may have concretely failed to reduce the type, or may simply be stuck /// may have concretely failed to reduce the type, or may simply be stuck
/// without more information. /// without more information.
@ -53,9 +93,7 @@ struct TypeFamily
std::string name; std::string name;
/// The reducer function for the type family. /// The reducer function for the type family.
std::function<TypeFamilyReductionResult<TypeId>(std::vector<TypeId>, std::vector<TypePackId>, NotNull<TypeArena>, NotNull<BuiltinTypes>, std::function<TypeFamilyReductionResult<TypeId>(std::vector<TypeId>, std::vector<TypePackId>, NotNull<TypeFamilyContext>)> reducer;
NotNull<TxnLog>, NotNull<Scope>, NotNull<Normalizer>, ConstraintSolver*)>
reducer;
}; };
/// Represents a type function that may be applied to map a series of types and /// Represents a type function that may be applied to map a series of types and
@ -67,9 +105,7 @@ struct TypePackFamily
std::string name; std::string name;
/// The reducer function for the type pack family. /// The reducer function for the type pack family.
std::function<TypeFamilyReductionResult<TypePackId>(std::vector<TypeId>, std::vector<TypePackId>, NotNull<TypeArena>, NotNull<BuiltinTypes>, std::function<TypeFamilyReductionResult<TypePackId>(std::vector<TypeId>, std::vector<TypePackId>, NotNull<TypeFamilyContext>)> reducer;
NotNull<TxnLog>, NotNull<Scope>, NotNull<Normalizer>, ConstraintSolver*)>
reducer;
}; };
struct FamilyGraphReductionResult struct FamilyGraphReductionResult
@ -82,76 +118,53 @@ struct FamilyGraphReductionResult
}; };
/** /**
* Attempt to reduce all instances of any type or type pack family in the type * Attempt to reduce all instances of any type or type pack family in the type
* graph provided. * graph provided.
* *
* @param entrypoint the entry point to the type graph. * @param entrypoint the entry point to the type graph.
* @param location the location the reduction is occurring at; used to populate * @param location the location the reduction is occurring at; used to populate
* type errors. * type errors.
* @param arena an arena to allocate types into. * @param arena an arena to allocate types into.
* @param builtins the built-in types. * @param builtins the built-in types.
* @param normalizer the normalizer to use when normalizing types * @param normalizer the normalizer to use when normalizing types
* @param log a TxnLog to use. If one is provided, substitution will take place * @param ice the internal error reporter to use for ICEs
* against the TxnLog, otherwise substitutions will directly mutate the type */
* graph. Do not provide the empty TxnLog, as a result. FamilyGraphReductionResult reduceFamilies(TypeId entrypoint, Location location, TypeFamilyContext, bool force = false);
*/
FamilyGraphReductionResult reduceFamilies(TypeId entrypoint, Location location, NotNull<TypeArena> arena, NotNull<BuiltinTypes> builtins,
NotNull<Scope> scope, NotNull<Normalizer> normalizer, TxnLog* log = nullptr, bool force = false);
/**
* Attempt to reduce all instances of any type or type pack family in the type
* graph provided.
*
* @param entrypoint the entry point to the type graph.
* @param location the location the reduction is occurring at; used to populate
* type errors.
* @param arena an arena to allocate types into.
* @param builtins the built-in types.
* @param normalizer the normalizer to use when normalizing types
* @param log a TxnLog to use. If one is provided, substitution will take place
* against the TxnLog, otherwise substitutions will directly mutate the type
* graph. Do not provide the empty TxnLog, as a result.
*/
FamilyGraphReductionResult reduceFamilies(TypePackId entrypoint, Location location, NotNull<TypeArena> arena, NotNull<BuiltinTypes> builtins,
NotNull<Scope> scope, NotNull<Normalizer> normalizer, TxnLog* log = nullptr, bool force = false);
/** /**
* Attempt to reduce all instances of any type or type pack family in the type * Attempt to reduce all instances of any type or type pack family in the type
* graph provided. * graph provided.
* *
* @param solver the constraint solver this reduction is being performed in.
* @param entrypoint the entry point to the type graph. * @param entrypoint the entry point to the type graph.
* @param location the location the reduction is occurring at; used to populate * @param location the location the reduction is occurring at; used to populate
* type errors. * type errors.
* @param log a TxnLog to use. If one is provided, substitution will take place * @param arena an arena to allocate types into.
* against the TxnLog, otherwise substitutions will directly mutate the type * @param builtins the built-in types.
* graph. Do not provide the empty TxnLog, as a result. * @param normalizer the normalizer to use when normalizing types
* @param ice the internal error reporter to use for ICEs
*/ */
FamilyGraphReductionResult reduceFamilies( FamilyGraphReductionResult reduceFamilies(TypePackId entrypoint, Location location, TypeFamilyContext, bool force = false);
NotNull<ConstraintSolver> solver, TypeId entrypoint, Location location, NotNull<Scope> scope, TxnLog* log = nullptr, bool force = false);
/**
* Attempt to reduce all instances of any type or type pack family in the type
* graph provided.
*
* @param solver the constraint solver this reduction is being performed in.
* @param entrypoint the entry point to the type graph.
* @param location the location the reduction is occurring at; used to populate
* type errors.
* @param log a TxnLog to use. If one is provided, substitution will take place
* against the TxnLog, otherwise substitutions will directly mutate the type
* graph. Do not provide the empty TxnLog, as a result.
*/
FamilyGraphReductionResult reduceFamilies(
NotNull<ConstraintSolver> solver, TypePackId entrypoint, Location location, NotNull<Scope> scope, TxnLog* log = nullptr, bool force = false);
struct BuiltinTypeFamilies struct BuiltinTypeFamilies
{ {
BuiltinTypeFamilies(); BuiltinTypeFamilies();
TypeFamily addFamily; TypeFamily addFamily;
TypeFamily subFamily;
TypeFamily mulFamily;
TypeFamily divFamily;
TypeFamily idivFamily;
TypeFamily powFamily;
TypeFamily modFamily;
TypeFamily andFamily;
TypeFamily orFamily;
void addToScope(NotNull<TypeArena> arena, NotNull<Scope> scope) const;
}; };
const BuiltinTypeFamilies kBuiltinTypeFamilies{}; const BuiltinTypeFamilies kBuiltinTypeFamilies{};
} // namespace Luau } // namespace Luau

View file

@ -8,7 +8,9 @@
#include "Luau/ToString.h" #include "Luau/ToString.h"
#include "Luau/ConstraintSolver.h" #include "Luau/ConstraintSolver.h"
#include "Luau/ConstraintGraphBuilder.h" #include "Luau/ConstraintGraphBuilder.h"
#include "Luau/NotNull.h"
#include "Luau/TypeInfer.h" #include "Luau/TypeInfer.h"
#include "Luau/TypeFamily.h"
#include "Luau/TypePack.h" #include "Luau/TypePack.h"
#include "Luau/Type.h" #include "Luau/Type.h"
#include "Luau/TypeUtils.h" #include "Luau/TypeUtils.h"
@ -21,6 +23,8 @@
* about a function that takes any number of values, but where each value must have some specific type. * about a function that takes any number of values, but where each value must have some specific type.
*/ */
LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution)
namespace Luau namespace Luau
{ {
@ -209,6 +213,9 @@ void registerBuiltinGlobals(Frontend& frontend, GlobalTypes& globals, bool typeC
TypeArena& arena = globals.globalTypes; TypeArena& arena = globals.globalTypes;
NotNull<BuiltinTypes> builtinTypes = globals.builtinTypes; NotNull<BuiltinTypes> builtinTypes = globals.builtinTypes;
if (FFlag::DebugLuauDeferredConstraintResolution)
kBuiltinTypeFamilies.addToScope(NotNull{&arena}, NotNull{globals.globalScope.get()});
LoadDefinitionFileResult loadResult = frontend.loadDefinitionFile( LoadDefinitionFileResult loadResult = frontend.loadDefinitionFile(
globals, globals.globalScope, getBuiltinDefinitionSource(), "@luau", /* captureComments */ false, typeCheckForAutocomplete); globals, globals.globalScope, getBuiltinDefinitionSource(), "@luau", /* captureComments */ false, typeCheckForAutocomplete);
LUAU_ASSERT(loadResult.success); LUAU_ASSERT(loadResult.success);

View file

@ -455,7 +455,7 @@ void ConstraintGraphBuilder::applyRefinements(const ScopePtr& scope, Location lo
} }
} }
scope->dcrRefinements[def] = ty; scope->rvalueRefinements[def] = ty;
} }
} }
@ -627,7 +627,7 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatLocal* l
if (hasAnnotation) if (hasAnnotation)
expectedType = varTypes.at(i); expectedType = varTypes.at(i);
TypeId exprType = check(scope, value, ValueContext::RValue, expectedType, /*forceSingleton*/ false, /*generalize*/ true).ty; TypeId exprType = check(scope, value, expectedType, /*forceSingleton*/ false, /*generalize*/ true).ty;
if (i < varTypes.size()) if (i < varTypes.size())
{ {
if (varTypes[i]) if (varTypes[i])
@ -699,7 +699,8 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatLocal* l
// HACK: In the greedy solver, we say the type state of a variable is the type annotation itself, but // HACK: In the greedy solver, we say the type state of a variable is the type annotation itself, but
// the actual type state is the corresponding initializer expression (if it exists) or nil otherwise. // the actual type state is the corresponding initializer expression (if it exists) or nil otherwise.
BreadcrumbId bc = dfg->getBreadcrumb(l); BreadcrumbId bc = dfg->getBreadcrumb(l);
scope->dcrRefinements[bc->def] = varTypes[i]; scope->lvalueTypes[bc->def] = varTypes[i];
scope->rvalueRefinements[bc->def] = varTypes[i];
} }
if (local->values.size > 0) if (local->values.size > 0)
@ -764,7 +765,8 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatFor* for
forScope->bindings[for_->var] = Binding{annotationTy, for_->var->location}; forScope->bindings[for_->var] = Binding{annotationTy, for_->var->location};
BreadcrumbId bc = dfg->getBreadcrumb(for_->var); BreadcrumbId bc = dfg->getBreadcrumb(for_->var);
forScope->dcrRefinements[bc->def] = annotationTy; forScope->lvalueTypes[bc->def] = annotationTy;
forScope->rvalueRefinements[bc->def] = annotationTy;
visit(forScope, for_->body); visit(forScope, for_->body);
@ -791,7 +793,8 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatForIn* f
variableTypes.push_back(ty); variableTypes.push_back(ty);
BreadcrumbId bc = dfg->getBreadcrumb(var); BreadcrumbId bc = dfg->getBreadcrumb(var);
loopScope->dcrRefinements[bc->def] = ty; loopScope->lvalueTypes[bc->def] = ty;
loopScope->rvalueRefinements[bc->def] = ty;
} }
// It is always ok to provide too few variables, so we give this pack a free tail. // It is always ok to provide too few variables, so we give this pack a free tail.
@ -845,8 +848,10 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatLocalFun
sig.bodyScope->bindings[function->name] = Binding{sig.signature, function->func->location}; sig.bodyScope->bindings[function->name] = Binding{sig.signature, function->func->location};
BreadcrumbId bc = dfg->getBreadcrumb(function->name); BreadcrumbId bc = dfg->getBreadcrumb(function->name);
scope->dcrRefinements[bc->def] = functionType; scope->lvalueTypes[bc->def] = functionType;
sig.bodyScope->dcrRefinements[bc->def] = sig.signature; scope->rvalueRefinements[bc->def] = functionType;
sig.bodyScope->lvalueTypes[bc->def] = sig.signature;
sig.bodyScope->rvalueRefinements[bc->def] = sig.signature;
Checkpoint start = checkpoint(this); Checkpoint start = checkpoint(this);
checkFunctionBody(sig.bodyScope, function->func); checkFunctionBody(sig.bodyScope, function->func);
@ -889,9 +894,12 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatFunction
const NullableBreadcrumbId functionBreadcrumb = dfg->getBreadcrumb(function->name); const NullableBreadcrumbId functionBreadcrumb = dfg->getBreadcrumb(function->name);
std::optional<TypeId> existingFunctionTy;
if (functionBreadcrumb)
existingFunctionTy = scope->lookupLValue(functionBreadcrumb->def);
if (AstExprLocal* localName = function->name->as<AstExprLocal>()) if (AstExprLocal* localName = function->name->as<AstExprLocal>())
{ {
std::optional<TypeId> existingFunctionTy = scope->lookup(localName->local);
if (existingFunctionTy) if (existingFunctionTy)
{ {
addConstraint(scope, function->name->location, SubtypeConstraint{generalizedType, *existingFunctionTy}); addConstraint(scope, function->name->location, SubtypeConstraint{generalizedType, *existingFunctionTy});
@ -905,11 +913,13 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatFunction
sig.bodyScope->bindings[localName->local] = Binding{sig.signature, localName->location}; sig.bodyScope->bindings[localName->local] = Binding{sig.signature, localName->location};
if (functionBreadcrumb) if (functionBreadcrumb)
sig.bodyScope->dcrRefinements[functionBreadcrumb->def] = sig.signature; {
sig.bodyScope->lvalueTypes[functionBreadcrumb->def] = sig.signature;
sig.bodyScope->rvalueRefinements[functionBreadcrumb->def] = sig.signature;
}
} }
else if (AstExprGlobal* globalName = function->name->as<AstExprGlobal>()) else if (AstExprGlobal* globalName = function->name->as<AstExprGlobal>())
{ {
std::optional<TypeId> existingFunctionTy = scope->lookup(globalName->name);
if (!existingFunctionTy) if (!existingFunctionTy)
ice->ice("prepopulateGlobalScope did not populate a global name", globalName->location); ice->ice("prepopulateGlobalScope did not populate a global name", globalName->location);
@ -918,7 +928,10 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatFunction
sig.bodyScope->bindings[globalName->name] = Binding{sig.signature, globalName->location}; sig.bodyScope->bindings[globalName->name] = Binding{sig.signature, globalName->location};
if (functionBreadcrumb) if (functionBreadcrumb)
sig.bodyScope->dcrRefinements[functionBreadcrumb->def] = sig.signature; {
sig.bodyScope->lvalueTypes[functionBreadcrumb->def] = sig.signature;
sig.bodyScope->rvalueRefinements[functionBreadcrumb->def] = sig.signature;
}
} }
else if (AstExprIndexName* indexName = function->name->as<AstExprIndexName>()) else if (AstExprIndexName* indexName = function->name->as<AstExprIndexName>())
{ {
@ -946,7 +959,7 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatFunction
ice->ice("generalizedType == nullptr", function->location); ice->ice("generalizedType == nullptr", function->location);
if (functionBreadcrumb) if (functionBreadcrumb)
scope->dcrRefinements[functionBreadcrumb->def] = generalizedType; scope->rvalueRefinements[functionBreadcrumb->def] = generalizedType;
checkFunctionBody(sig.bodyScope, function->func); checkFunctionBody(sig.bodyScope, function->func);
Checkpoint end = checkpoint(this); Checkpoint end = checkpoint(this);
@ -1059,7 +1072,7 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatCompound
ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatIf* ifStatement) ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatIf* ifStatement)
{ {
RefinementId refinement = check(scope, ifStatement->condition, ValueContext::RValue, std::nullopt).refinement; RefinementId refinement = check(scope, ifStatement->condition, std::nullopt).refinement;
ScopePtr thenScope = childScope(ifStatement->thenbody, scope); ScopePtr thenScope = childScope(ifStatement->thenbody, scope);
applyRefinements(thenScope, ifStatement->condition->location, refinement); applyRefinements(thenScope, ifStatement->condition->location, refinement);
@ -1164,7 +1177,8 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatDeclareG
rootScope->bindings[global->name] = Binding{globalTy, global->location}; rootScope->bindings[global->name] = Binding{globalTy, global->location};
BreadcrumbId bc = dfg->getBreadcrumb(global); BreadcrumbId bc = dfg->getBreadcrumb(global);
rootScope->dcrRefinements[bc->def] = globalTy; rootScope->lvalueTypes[bc->def] = globalTy;
rootScope->rvalueRefinements[bc->def] = globalTy;
return ControlFlow::None; return ControlFlow::None;
} }
@ -1323,6 +1337,7 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatDeclareF
TypePackId retPack = resolveTypePack(funScope, global->retTypes, /* inTypeArguments */ false); TypePackId retPack = resolveTypePack(funScope, global->retTypes, /* inTypeArguments */ false);
TypeId fnType = arena->addType(FunctionType{TypeLevel{}, funScope.get(), std::move(genericTys), std::move(genericTps), paramPack, retPack}); TypeId fnType = arena->addType(FunctionType{TypeLevel{}, funScope.get(), std::move(genericTys), std::move(genericTps), paramPack, retPack});
FunctionType* ftv = getMutable<FunctionType>(fnType); FunctionType* ftv = getMutable<FunctionType>(fnType);
ftv->isCheckedFunction = global->checkedFunction;
ftv->argNames.reserve(global->paramNames.size); ftv->argNames.reserve(global->paramNames.size);
for (const auto& el : global->paramNames) for (const auto& el : global->paramNames)
@ -1334,7 +1349,8 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatDeclareF
scope->bindings[global->name] = Binding{fnType, global->location}; scope->bindings[global->name] = Binding{fnType, global->location};
BreadcrumbId bc = dfg->getBreadcrumb(global); BreadcrumbId bc = dfg->getBreadcrumb(global);
rootScope->dcrRefinements[bc->def] = fnType; rootScope->lvalueTypes[bc->def] = fnType;
rootScope->rvalueRefinements[bc->def] = fnType;
return ControlFlow::None; return ControlFlow::None;
} }
@ -1363,7 +1379,7 @@ InferencePack ConstraintGraphBuilder::checkPack(
std::optional<TypeId> expectedType; std::optional<TypeId> expectedType;
if (i < expectedTypes.size()) if (i < expectedTypes.size())
expectedType = expectedTypes[i]; expectedType = expectedTypes[i];
head.push_back(check(scope, expr, ValueContext::RValue, expectedType).ty); head.push_back(check(scope, expr, expectedType).ty);
} }
else else
{ {
@ -1407,7 +1423,7 @@ InferencePack ConstraintGraphBuilder::checkPack(
std::optional<TypeId> expectedType; std::optional<TypeId> expectedType;
if (!expectedTypes.empty()) if (!expectedTypes.empty())
expectedType = expectedTypes[0]; expectedType = expectedTypes[0];
TypeId t = check(scope, expr, ValueContext::RValue, expectedType, /*forceSingletons*/ false, generalize).ty; TypeId t = check(scope, expr, expectedType, /*forceSingletons*/ false, generalize).ty;
result = InferencePack{arena->addTypePack({t})}; result = InferencePack{arena->addTypePack({t})};
} }
@ -1489,8 +1505,7 @@ InferencePack ConstraintGraphBuilder::checkPack(const ScopePtr& scope, AstExprCa
} }
else if (i < exprArgs.size() - 1 || !(arg->is<AstExprCall>() || arg->is<AstExprVarargs>())) else if (i < exprArgs.size() - 1 || !(arg->is<AstExprCall>() || arg->is<AstExprVarargs>()))
{ {
auto [ty, refinement] = auto [ty, refinement] = check(scope, arg, /*expectedType*/ std::nullopt, /*forceSingleton*/ false, /*generalize*/ false);
check(scope, arg, ValueContext::RValue, /*expectedType*/ std::nullopt, /*forceSingleton*/ false, /*generalize*/ false);
args.push_back(ty); args.push_back(ty);
argumentRefinements.push_back(refinement); argumentRefinements.push_back(refinement);
} }
@ -1549,7 +1564,8 @@ InferencePack ConstraintGraphBuilder::checkPack(const ScopePtr& scope, AstExprCa
scope->bindings[targetLocal->local].typeId = resultTy; scope->bindings[targetLocal->local].typeId = resultTy;
BreadcrumbId bc = dfg->getBreadcrumb(targetLocal); BreadcrumbId bc = dfg->getBreadcrumb(targetLocal);
scope->dcrRefinements[bc->def] = resultTy; // TODO: typestates: track this as an assignment scope->lvalueTypes[bc->def] = resultTy; // TODO: typestates: track this as an assignment
scope->rvalueRefinements[bc->def] = resultTy; // TODO: typestates: track this as an assignment
} }
return InferencePack{arena->addTypePack({resultTy}), {refinementArena.variadic(returnRefinements)}}; return InferencePack{arena->addTypePack({resultTy}), {refinementArena.variadic(returnRefinements)}};
@ -1588,7 +1604,7 @@ InferencePack ConstraintGraphBuilder::checkPack(const ScopePtr& scope, AstExprCa
} }
Inference ConstraintGraphBuilder::check( Inference ConstraintGraphBuilder::check(
const ScopePtr& scope, AstExpr* expr, ValueContext context, std::optional<TypeId> expectedType, bool forceSingleton, bool generalize) const ScopePtr& scope, AstExpr* expr, std::optional<TypeId> expectedType, bool forceSingleton, bool generalize)
{ {
RecursionCounter counter{&recursionCount}; RecursionCounter counter{&recursionCount};
@ -1601,7 +1617,7 @@ Inference ConstraintGraphBuilder::check(
Inference result; Inference result;
if (auto group = expr->as<AstExprGroup>()) if (auto group = expr->as<AstExprGroup>())
result = check(scope, group->expr, ValueContext::RValue, expectedType, forceSingleton); result = check(scope, group->expr, expectedType, forceSingleton);
else if (auto stringExpr = expr->as<AstExprConstantString>()) else if (auto stringExpr = expr->as<AstExprConstantString>())
result = check(scope, stringExpr, expectedType, forceSingleton); result = check(scope, stringExpr, expectedType, forceSingleton);
else if (expr->is<AstExprConstantNumber>()) else if (expr->is<AstExprConstantNumber>())
@ -1611,7 +1627,7 @@ Inference ConstraintGraphBuilder::check(
else if (expr->is<AstExprConstantNil>()) else if (expr->is<AstExprConstantNil>())
result = Inference{builtinTypes->nilType}; result = Inference{builtinTypes->nilType};
else if (auto local = expr->as<AstExprLocal>()) else if (auto local = expr->as<AstExprLocal>())
result = check(scope, local, context); result = check(scope, local);
else if (auto global = expr->as<AstExprGlobal>()) else if (auto global = expr->as<AstExprGlobal>())
result = check(scope, global); result = check(scope, global);
else if (expr->is<AstExprVarargs>()) else if (expr->is<AstExprVarargs>())
@ -1706,13 +1722,11 @@ Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprConstantBo
return Inference{builtinTypes->booleanType}; return Inference{builtinTypes->booleanType};
} }
Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprLocal* local, ValueContext context) Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprLocal* local)
{ {
BreadcrumbId bc = dfg->getBreadcrumb(local); BreadcrumbId bc = dfg->getBreadcrumb(local);
if (auto ty = scope->lookup(bc->def); ty && context == ValueContext::RValue) if (auto ty = scope->lookup(bc->def))
return Inference{*ty, refinementArena.proposition(bc, builtinTypes->truthyType)};
else if (auto ty = scope->lookup(local->local))
return Inference{*ty, refinementArena.proposition(bc, builtinTypes->truthyType)}; return Inference{*ty, refinementArena.proposition(bc, builtinTypes->truthyType)};
else else
ice->ice("AstExprLocal came before its declaration?"); ice->ice("AstExprLocal came before its declaration?");
@ -1729,7 +1743,7 @@ Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprGlobal* gl
return Inference{*ty, refinementArena.proposition(bc, builtinTypes->truthyType)}; return Inference{*ty, refinementArena.proposition(bc, builtinTypes->truthyType)};
else if (auto ty = scope->lookup(global->name)) else if (auto ty = scope->lookup(global->name))
{ {
rootScope->dcrRefinements[bc->def] = *ty; rootScope->rvalueRefinements[bc->def] = *ty;
return Inference{*ty, refinementArena.proposition(bc, builtinTypes->truthyType)}; return Inference{*ty, refinementArena.proposition(bc, builtinTypes->truthyType)};
} }
else else
@ -1750,7 +1764,7 @@ Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprIndexName*
if (auto ty = scope->lookup(bc->def)) if (auto ty = scope->lookup(bc->def))
return Inference{*ty, refinementArena.proposition(NotNull{bc}, builtinTypes->truthyType)}; return Inference{*ty, refinementArena.proposition(NotNull{bc}, builtinTypes->truthyType)};
scope->dcrRefinements[bc->def] = result; scope->rvalueRefinements[bc->def] = result;
} }
addConstraint(scope, indexName->expr->location, HasPropConstraint{result, obj, indexName->index.value}); addConstraint(scope, indexName->expr->location, HasPropConstraint{result, obj, indexName->index.value});
@ -1773,7 +1787,7 @@ Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprIndexExpr*
if (auto ty = scope->lookup(bc->def)) if (auto ty = scope->lookup(bc->def))
return Inference{*ty, refinementArena.proposition(NotNull{bc}, builtinTypes->truthyType)}; return Inference{*ty, refinementArena.proposition(NotNull{bc}, builtinTypes->truthyType)};
scope->dcrRefinements[bc->def] = result; scope->rvalueRefinements[bc->def] = result;
} }
TableIndexer indexer{indexType, result}; TableIndexer indexer{indexType, result};
@ -1836,7 +1850,9 @@ Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprBinary* bi
{ {
auto [leftType, rightType, refinement] = checkBinary(scope, binary, expectedType); auto [leftType, rightType, refinement] = checkBinary(scope, binary, expectedType);
if (binary->op == AstExprBinary::Op::Add) switch (binary->op)
{
case AstExprBinary::Op::Add:
{ {
TypeId resultType = arena->addType(TypeFamilyInstanceType{ TypeId resultType = arena->addType(TypeFamilyInstanceType{
NotNull{&kBuiltinTypeFamilies.addFamily}, NotNull{&kBuiltinTypeFamilies.addFamily},
@ -1846,11 +1862,94 @@ Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprBinary* bi
addConstraint(scope, binary->location, ReduceConstraint{resultType}); addConstraint(scope, binary->location, ReduceConstraint{resultType});
return Inference{resultType, std::move(refinement)}; return Inference{resultType, std::move(refinement)};
} }
case AstExprBinary::Op::Sub:
TypeId resultType = arena->addType(BlockedType{}); {
addConstraint(scope, binary->location, TypeId resultType = arena->addType(TypeFamilyInstanceType{
BinaryConstraint{binary->op, leftType, rightType, resultType, binary, &module->astOriginalCallTypes, &module->astOverloadResolvedTypes}); NotNull{&kBuiltinTypeFamilies.subFamily},
return Inference{resultType, std::move(refinement)}; {leftType, rightType},
{},
});
addConstraint(scope, binary->location, ReduceConstraint{resultType});
return Inference{resultType, std::move(refinement)};
}
case AstExprBinary::Op::Mul:
{
TypeId resultType = arena->addType(TypeFamilyInstanceType{
NotNull{&kBuiltinTypeFamilies.mulFamily},
{leftType, rightType},
{},
});
addConstraint(scope, binary->location, ReduceConstraint{resultType});
return Inference{resultType, std::move(refinement)};
}
case AstExprBinary::Op::Div:
{
TypeId resultType = arena->addType(TypeFamilyInstanceType{
NotNull{&kBuiltinTypeFamilies.divFamily},
{leftType, rightType},
{},
});
addConstraint(scope, binary->location, ReduceConstraint{resultType});
return Inference{resultType, std::move(refinement)};
}
case AstExprBinary::Op::FloorDiv:
{
TypeId resultType = arena->addType(TypeFamilyInstanceType{
NotNull{&kBuiltinTypeFamilies.idivFamily},
{leftType, rightType},
{},
});
addConstraint(scope, binary->location, ReduceConstraint{resultType});
return Inference{resultType, std::move(refinement)};
}
case AstExprBinary::Op::Pow:
{
TypeId resultType = arena->addType(TypeFamilyInstanceType{
NotNull{&kBuiltinTypeFamilies.powFamily},
{leftType, rightType},
{},
});
addConstraint(scope, binary->location, ReduceConstraint{resultType});
return Inference{resultType, std::move(refinement)};
}
case AstExprBinary::Op::Mod:
{
TypeId resultType = arena->addType(TypeFamilyInstanceType{
NotNull{&kBuiltinTypeFamilies.modFamily},
{leftType, rightType},
{},
});
addConstraint(scope, binary->location, ReduceConstraint{resultType});
return Inference{resultType, std::move(refinement)};
}
case AstExprBinary::Op::And:
{
TypeId resultType = arena->addType(TypeFamilyInstanceType{
NotNull{&kBuiltinTypeFamilies.andFamily},
{leftType, rightType},
{},
});
addConstraint(scope, binary->location, ReduceConstraint{resultType});
return Inference{resultType, std::move(refinement)};
}
case AstExprBinary::Op::Or:
{
TypeId resultType = arena->addType(TypeFamilyInstanceType{
NotNull{&kBuiltinTypeFamilies.orFamily},
{leftType, rightType},
{},
});
addConstraint(scope, binary->location, ReduceConstraint{resultType});
return Inference{resultType, std::move(refinement)};
}
default:
{
TypeId resultType = arena->addType(BlockedType{});
addConstraint(scope, binary->location,
BinaryConstraint{binary->op, leftType, rightType, resultType, binary, &module->astOriginalCallTypes, &module->astOverloadResolvedTypes});
return Inference{resultType, std::move(refinement)};
}
}
} }
Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprIfElse* ifElse, std::optional<TypeId> expectedType) Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprIfElse* ifElse, std::optional<TypeId> expectedType)
@ -1860,18 +1959,18 @@ Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprIfElse* if
ScopePtr thenScope = childScope(ifElse->trueExpr, scope); ScopePtr thenScope = childScope(ifElse->trueExpr, scope);
applyRefinements(thenScope, ifElse->trueExpr->location, refinement); applyRefinements(thenScope, ifElse->trueExpr->location, refinement);
TypeId thenType = check(thenScope, ifElse->trueExpr, ValueContext::RValue, expectedType).ty; TypeId thenType = check(thenScope, ifElse->trueExpr, expectedType).ty;
ScopePtr elseScope = childScope(ifElse->falseExpr, scope); ScopePtr elseScope = childScope(ifElse->falseExpr, scope);
applyRefinements(elseScope, ifElse->falseExpr->location, refinementArena.negation(refinement)); applyRefinements(elseScope, ifElse->falseExpr->location, refinementArena.negation(refinement));
TypeId elseType = check(elseScope, ifElse->falseExpr, ValueContext::RValue, expectedType).ty; TypeId elseType = check(elseScope, ifElse->falseExpr, expectedType).ty;
return Inference{expectedType ? *expectedType : simplifyUnion(builtinTypes, arena, thenType, elseType).result}; return Inference{expectedType ? *expectedType : simplifyUnion(builtinTypes, arena, thenType, elseType).result};
} }
Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprTypeAssertion* typeAssert) Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprTypeAssertion* typeAssert)
{ {
check(scope, typeAssert->expr, ValueContext::RValue, std::nullopt); check(scope, typeAssert->expr, std::nullopt);
return Inference{resolveType(scope, typeAssert->annotation, /* inTypeArguments */ false)}; return Inference{resolveType(scope, typeAssert->annotation, /* inTypeArguments */ false)};
} }
@ -1893,11 +1992,11 @@ std::tuple<TypeId, TypeId, RefinementId> ConstraintGraphBuilder::checkBinary(
if (expectedType) if (expectedType)
relaxedExpectedLhs = arena->addType(UnionType{{builtinTypes->falsyType, *expectedType}}); relaxedExpectedLhs = arena->addType(UnionType{{builtinTypes->falsyType, *expectedType}});
auto [leftType, leftRefinement] = check(scope, binary->left, ValueContext::RValue, relaxedExpectedLhs); auto [leftType, leftRefinement] = check(scope, binary->left, relaxedExpectedLhs);
ScopePtr rightScope = childScope(binary->right, scope); ScopePtr rightScope = childScope(binary->right, scope);
applyRefinements(rightScope, binary->right->location, leftRefinement); applyRefinements(rightScope, binary->right->location, leftRefinement);
auto [rightType, rightRefinement] = check(rightScope, binary->right, ValueContext::RValue, expectedType); auto [rightType, rightRefinement] = check(rightScope, binary->right, expectedType);
return {leftType, rightType, refinementArena.conjunction(leftRefinement, rightRefinement)}; return {leftType, rightType, refinementArena.conjunction(leftRefinement, rightRefinement)};
} }
@ -1908,11 +2007,11 @@ std::tuple<TypeId, TypeId, RefinementId> ConstraintGraphBuilder::checkBinary(
if (expectedType) if (expectedType)
relaxedExpectedLhs = arena->addType(UnionType{{builtinTypes->falsyType, *expectedType}}); relaxedExpectedLhs = arena->addType(UnionType{{builtinTypes->falsyType, *expectedType}});
auto [leftType, leftRefinement] = check(scope, binary->left, ValueContext::RValue, relaxedExpectedLhs); auto [leftType, leftRefinement] = check(scope, binary->left, relaxedExpectedLhs);
ScopePtr rightScope = childScope(binary->right, scope); ScopePtr rightScope = childScope(binary->right, scope);
applyRefinements(rightScope, binary->right->location, refinementArena.negation(leftRefinement)); applyRefinements(rightScope, binary->right->location, refinementArena.negation(leftRefinement));
auto [rightType, rightRefinement] = check(rightScope, binary->right, ValueContext::RValue, expectedType); auto [rightType, rightRefinement] = check(rightScope, binary->right, expectedType);
return {leftType, rightType, refinementArena.disjunction(leftRefinement, rightRefinement)}; return {leftType, rightType, refinementArena.disjunction(leftRefinement, rightRefinement)};
} }
@ -1973,8 +2072,8 @@ std::tuple<TypeId, TypeId, RefinementId> ConstraintGraphBuilder::checkBinary(
{ {
// We are checking a binary expression of the form a op b // We are checking a binary expression of the form a op b
// Just because a op b is epxected to return a bool, doesn't mean a, b are expected to be bools too // Just because a op b is epxected to return a bool, doesn't mean a, b are expected to be bools too
TypeId leftType = check(scope, binary->left, ValueContext::RValue, {}, true).ty; TypeId leftType = check(scope, binary->left, {}, true).ty;
TypeId rightType = check(scope, binary->right, ValueContext::RValue, {}, true).ty; TypeId rightType = check(scope, binary->right, {}, true).ty;
RefinementId leftRefinement = nullptr; RefinementId leftRefinement = nullptr;
if (auto bc = dfg->getBreadcrumb(binary->left)) if (auto bc = dfg->getBreadcrumb(binary->left))
@ -1994,8 +2093,8 @@ std::tuple<TypeId, TypeId, RefinementId> ConstraintGraphBuilder::checkBinary(
} }
else else
{ {
TypeId leftType = check(scope, binary->left, ValueContext::RValue).ty; TypeId leftType = check(scope, binary->left).ty;
TypeId rightType = check(scope, binary->right, ValueContext::RValue).ty; TypeId rightType = check(scope, binary->right).ty;
return {leftType, rightType, nullptr}; return {leftType, rightType, nullptr};
} }
} }
@ -2011,6 +2110,47 @@ std::vector<TypeId> ConstraintGraphBuilder::checkLValues(const ScopePtr& scope,
return types; return types;
} }
TypeId ConstraintGraphBuilder::checkLValue(const ScopePtr& scope, AstExpr* expr)
{
if (auto local = expr->as<AstExprLocal>())
return checkLValue(scope, local);
else if (auto global = expr->as<AstExprGlobal>())
return checkLValue(scope, global);
else if (auto indexName = expr->as<AstExprIndexName>())
return checkLValue(scope, indexName);
else if (auto indexExpr = expr->as<AstExprIndexExpr>())
return checkLValue(scope, indexExpr);
else if (auto error = expr->as<AstExprError>())
{
check(scope, error);
return builtinTypes->errorRecoveryType();
}
else
ice->ice("checkLValue is inexhaustive");
}
TypeId ConstraintGraphBuilder::checkLValue(const ScopePtr& scope, AstExprLocal* local)
{
std::optional<TypeId> upperBound = scope->lookup(Symbol{local->local});
LUAU_ASSERT(upperBound);
return *upperBound;
}
TypeId ConstraintGraphBuilder::checkLValue(const ScopePtr& scope, AstExprGlobal* global)
{
return scope->lookup(Symbol{global->name}).value_or(builtinTypes->errorRecoveryType());
}
TypeId ConstraintGraphBuilder::checkLValue(const ScopePtr& scope, AstExprIndexName* indexName)
{
return updateProperty(scope, indexName);
}
TypeId ConstraintGraphBuilder::checkLValue(const ScopePtr& scope, AstExprIndexExpr* indexExpr)
{
return updateProperty(scope, indexExpr);
}
static bool isIndexNameEquivalent(AstExpr* expr) static bool isIndexNameEquivalent(AstExpr* expr)
{ {
if (expr->is<AstExprIndexName>()) if (expr->is<AstExprIndexName>())
@ -2031,7 +2171,7 @@ static bool isIndexNameEquivalent(AstExpr* expr)
* *
* If expr has the form name.a.b.c * If expr has the form name.a.b.c
*/ */
TypeId ConstraintGraphBuilder::checkLValue(const ScopePtr& scope, AstExpr* expr) TypeId ConstraintGraphBuilder::updateProperty(const ScopePtr& scope, AstExpr* expr)
{ {
if (auto indexExpr = expr->as<AstExprIndexExpr>(); indexExpr && !indexExpr->index->is<AstExprConstantString>()) if (auto indexExpr = expr->as<AstExprIndexExpr>(); indexExpr && !indexExpr->index->is<AstExprConstantString>())
{ {
@ -2058,7 +2198,7 @@ TypeId ConstraintGraphBuilder::checkLValue(const ScopePtr& scope, AstExpr* expr)
return propType; return propType;
} }
else if (!isIndexNameEquivalent(expr)) else if (!isIndexNameEquivalent(expr))
return check(scope, expr, ValueContext::LValue).ty; return check(scope, expr).ty;
Symbol sym; Symbol sym;
std::vector<std::string> segments; std::vector<std::string> segments;
@ -2086,7 +2226,7 @@ TypeId ConstraintGraphBuilder::checkLValue(const ScopePtr& scope, AstExpr* expr)
else if (auto indexExpr = e->as<AstExprIndexExpr>()) else if (auto indexExpr = e->as<AstExprIndexExpr>())
{ {
// We need to populate the type for the index value // We need to populate the type for the index value
check(scope, indexExpr->index, ValueContext::RValue); check(scope, indexExpr->index);
if (auto strIndex = indexExpr->index->as<AstExprConstantString>()) if (auto strIndex = indexExpr->index->as<AstExprConstantString>())
{ {
segments.push_back(std::string(strIndex->value.data, strIndex->value.size)); segments.push_back(std::string(strIndex->value.data, strIndex->value.size));
@ -2095,11 +2235,11 @@ TypeId ConstraintGraphBuilder::checkLValue(const ScopePtr& scope, AstExpr* expr)
} }
else else
{ {
return check(scope, expr, ValueContext::LValue).ty; return check(scope, expr).ty;
} }
} }
else else
return check(scope, expr, ValueContext::LValue).ty; return check(scope, expr).ty;
} }
LUAU_ASSERT(!segments.empty()); LUAU_ASSERT(!segments.empty());
@ -2109,7 +2249,7 @@ TypeId ConstraintGraphBuilder::checkLValue(const ScopePtr& scope, AstExpr* expr)
auto lookupResult = scope->lookupEx(sym); auto lookupResult = scope->lookupEx(sym);
if (!lookupResult) if (!lookupResult)
return check(scope, expr, ValueContext::LValue).ty; return check(scope, expr).ty;
const auto [subjectBinding, symbolScope] = std::move(*lookupResult); const auto [subjectBinding, symbolScope] = std::move(*lookupResult);
TypeId subjectType = subjectBinding->typeId; TypeId subjectType = subjectBinding->typeId;
@ -2139,7 +2279,10 @@ TypeId ConstraintGraphBuilder::checkLValue(const ScopePtr& scope, AstExpr* expr)
// This can fail if the user is erroneously trying to augment a builtin // This can fail if the user is erroneously trying to augment a builtin
// table like os or string. // table like os or string.
if (auto bc = dfg->getBreadcrumb(e)) if (auto bc = dfg->getBreadcrumb(e))
symbolScope->dcrRefinements[bc->def] = updatedType; {
symbolScope->lvalueTypes[bc->def] = updatedType;
symbolScope->rvalueRefinements[bc->def] = updatedType;
}
} }
return propTy; return propTy;
@ -2232,7 +2375,7 @@ Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprTable* exp
checkExpectedIndexResultType = pinnedIndexResultType; checkExpectedIndexResultType = pinnedIndexResultType;
} }
TypeId itemTy = check(scope, item.value, ValueContext::RValue, checkExpectedIndexResultType).ty; TypeId itemTy = check(scope, item.value, checkExpectedIndexResultType).ty;
if (isIndexedResultType && !pinnedIndexResultType) if (isIndexedResultType && !pinnedIndexResultType)
pinnedIndexResultType = itemTy; pinnedIndexResultType = itemTy;
@ -2242,7 +2385,7 @@ Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprTable* exp
// Even though we don't need to use the type of the item's key if // Even though we don't need to use the type of the item's key if
// it's a string constant, we still want to check it to populate // it's a string constant, we still want to check it to populate
// astTypes. // astTypes.
TypeId keyTy = check(scope, item.key, ValueContext::RValue, annotatedKeyType).ty; TypeId keyTy = check(scope, item.key, annotatedKeyType).ty;
if (AstExprConstantString* key = item.key->as<AstExprConstantString>()) if (AstExprConstantString* key = item.key->as<AstExprConstantString>())
{ {
@ -2346,7 +2489,8 @@ ConstraintGraphBuilder::FunctionSignature ConstraintGraphBuilder::checkFunctionS
signatureScope->bindings[fn->self] = Binding{selfType, fn->self->location}; signatureScope->bindings[fn->self] = Binding{selfType, fn->self->location};
BreadcrumbId bc = dfg->getBreadcrumb(fn->self); BreadcrumbId bc = dfg->getBreadcrumb(fn->self);
signatureScope->dcrRefinements[bc->def] = selfType; signatureScope->lvalueTypes[bc->def] = selfType;
signatureScope->rvalueRefinements[bc->def] = selfType;
} }
for (size_t i = 0; i < fn->args.size; ++i) for (size_t i = 0; i < fn->args.size; ++i)
@ -2370,7 +2514,8 @@ ConstraintGraphBuilder::FunctionSignature ConstraintGraphBuilder::checkFunctionS
signatureScope->bindings[local] = Binding{argTy, local->location}; signatureScope->bindings[local] = Binding{argTy, local->location};
BreadcrumbId bc = dfg->getBreadcrumb(local); BreadcrumbId bc = dfg->getBreadcrumb(local);
signatureScope->dcrRefinements[bc->def] = argTy; signatureScope->lvalueTypes[bc->def] = argTy;
signatureScope->rvalueRefinements[bc->def] = argTy;
} }
TypePackId varargPack = nullptr; TypePackId varargPack = nullptr;
@ -2612,6 +2757,7 @@ TypeId ConstraintGraphBuilder::resolveType(const ScopePtr& scope, AstType* ty, b
// TODO: FunctionType needs a pointer to the scope so that we know // TODO: FunctionType needs a pointer to the scope so that we know
// how to quantify/instantiate it. // how to quantify/instantiate it.
FunctionType ftv{TypeLevel{}, scope.get(), {}, {}, argTypes, returnTypes}; FunctionType ftv{TypeLevel{}, scope.get(), {}, {}, argTypes, returnTypes};
ftv.isCheckedFunction = fn->checkedFunction;
// This replicates the behavior of the appropriate FunctionType // This replicates the behavior of the appropriate FunctionType
// constructors. // constructors.
@ -2836,17 +2982,27 @@ struct GlobalPrepopulator : AstVisitor
{ {
const NotNull<Scope> globalScope; const NotNull<Scope> globalScope;
const NotNull<TypeArena> arena; const NotNull<TypeArena> arena;
const NotNull<const DataFlowGraph> dfg;
GlobalPrepopulator(NotNull<Scope> globalScope, NotNull<TypeArena> arena) GlobalPrepopulator(NotNull<Scope> globalScope, NotNull<TypeArena> arena, NotNull<const DataFlowGraph> dfg)
: globalScope(globalScope) : globalScope(globalScope)
, arena(arena) , arena(arena)
, dfg(dfg)
{ {
} }
bool visit(AstStatFunction* function) override bool visit(AstStatFunction* function) override
{ {
if (AstExprGlobal* g = function->name->as<AstExprGlobal>()) if (AstExprGlobal* g = function->name->as<AstExprGlobal>())
globalScope->bindings[g->name] = Binding{arena->addType(BlockedType{})}; {
TypeId bt = arena->addType(BlockedType{});
globalScope->bindings[g->name] = Binding{bt};
NullableBreadcrumbId bc = dfg->getBreadcrumb(function->name);
LUAU_ASSERT(bc);
globalScope->lvalueTypes[bc->def] = bt;
}
return true; return true;
} }
@ -2854,7 +3010,7 @@ struct GlobalPrepopulator : AstVisitor
void ConstraintGraphBuilder::prepopulateGlobalScope(const ScopePtr& globalScope, AstStatBlock* program) void ConstraintGraphBuilder::prepopulateGlobalScope(const ScopePtr& globalScope, AstStatBlock* program)
{ {
GlobalPrepopulator gp{NotNull{globalScope.get()}, arena}; GlobalPrepopulator gp{NotNull{globalScope.get()}, arena, dfg};
if (prepareModuleScope) if (prepareModuleScope)
prepareModuleScope(module->name, globalScope); prepareModuleScope(module->name, globalScope);

View file

@ -1344,7 +1344,7 @@ bool ConstraintSolver::tryDispatch(const FunctionCallConstraint& c, NotNull<cons
head.insert(head.begin(), fn); head.insert(head.begin(), fn);
argsPack = arena->addTypePack(TypePack{std::move(head), tail}); argsPack = arena->addTypePack(TypePack{std::move(head), tail});
fn = *callMm; fn = follow(*callMm);
asMutable(c.result)->ty.emplace<FreeTypePack>(constraint->scope); asMutable(c.result)->ty.emplace<FreeTypePack>(constraint->scope);
} }
else else
@ -1887,6 +1887,9 @@ bool ConstraintSolver::tryDispatch(const RefineConstraint& c, NotNull<const Cons
const TypeId type = follow(c.type); const TypeId type = follow(c.type);
if (hasUnresolvedConstraints(type))
return block(type, constraint);
LUAU_ASSERT(get<BlockedType>(c.resultType)); LUAU_ASSERT(get<BlockedType>(c.resultType));
if (type == c.resultType) if (type == c.resultType)
@ -1946,8 +1949,7 @@ bool ConstraintSolver::tryDispatch(const RefineConstraint& c, NotNull<const Cons
bool ConstraintSolver::tryDispatch(const ReduceConstraint& c, NotNull<const Constraint> constraint, bool force) bool ConstraintSolver::tryDispatch(const ReduceConstraint& c, NotNull<const Constraint> constraint, bool force)
{ {
TypeId ty = follow(c.ty); TypeId ty = follow(c.ty);
FamilyGraphReductionResult result = FamilyGraphReductionResult result = reduceFamilies(ty, constraint->location, TypeFamilyContext{NotNull{this}, constraint->scope}, force);
reduceFamilies(NotNull{this}, ty, constraint->location, constraint->scope, nullptr, force);
for (TypeId r : result.reducedTypes) for (TypeId r : result.reducedTypes)
unblock(r, constraint->location); unblock(r, constraint->location);
@ -1970,8 +1972,7 @@ bool ConstraintSolver::tryDispatch(const ReduceConstraint& c, NotNull<const Cons
bool ConstraintSolver::tryDispatch(const ReducePackConstraint& c, NotNull<const Constraint> constraint, bool force) bool ConstraintSolver::tryDispatch(const ReducePackConstraint& c, NotNull<const Constraint> constraint, bool force)
{ {
TypePackId tp = follow(c.tp); TypePackId tp = follow(c.tp);
FamilyGraphReductionResult result = FamilyGraphReductionResult result = reduceFamilies(tp, constraint->location, TypeFamilyContext{NotNull{this}, constraint->scope}, force);
reduceFamilies(NotNull{this}, tp, constraint->location, constraint->scope, nullptr, force);
for (TypeId r : result.reducedTypes) for (TypeId r : result.reducedTypes)
unblock(r, constraint->location); unblock(r, constraint->location);

View file

@ -55,12 +55,25 @@ std::optional<std::pair<Binding*, Scope*>> Scope::lookupEx(Symbol sym)
} }
} }
std::optional<TypeId> Scope::lookupLValue(DefId def) const
{
for (const Scope* current = this; current; current = current->parent.get())
{
if (auto ty = current->lvalueTypes.find(def))
return *ty;
}
return std::nullopt;
}
// TODO: We might kill Scope::lookup(Symbol) once data flow is fully fleshed out with type states and control flow analysis. // TODO: We might kill Scope::lookup(Symbol) once data flow is fully fleshed out with type states and control flow analysis.
std::optional<TypeId> Scope::lookup(DefId def) const std::optional<TypeId> Scope::lookup(DefId def) const
{ {
for (const Scope* current = this; current; current = current->parent.get()) for (const Scope* current = this; current; current = current->parent.get())
{ {
if (auto ty = current->dcrRefinements.find(def)) if (auto ty = current->rvalueRefinements.find(def))
return *ty;
if (auto ty = current->lvalueTypes.find(def))
return *ty; return *ty;
} }
@ -156,10 +169,10 @@ void Scope::inheritRefinements(const ScopePtr& childScope)
{ {
if (FFlag::DebugLuauDeferredConstraintResolution) if (FFlag::DebugLuauDeferredConstraintResolution)
{ {
for (const auto& [k, a] : childScope->dcrRefinements) for (const auto& [k, a] : childScope->rvalueRefinements)
{ {
if (lookup(NotNull{k})) if (lookup(NotNull{k}))
dcrRefinements[k] = a; rvalueRefinements[k] = a;
} }
} }

View file

@ -101,6 +101,27 @@ SubtypingResult Subtyping::isSubtype(TypeId subTy, TypeId superTy)
TypeId lowerBound = makeAggregateType<UnionType>(lb, builtinTypes->neverType); TypeId lowerBound = makeAggregateType<UnionType>(lb, builtinTypes->neverType);
TypeId upperBound = makeAggregateType<IntersectionType>(ub, builtinTypes->unknownType); TypeId upperBound = makeAggregateType<IntersectionType>(ub, builtinTypes->unknownType);
const NormalizedType* nt = normalizer->normalize(upperBound);
if (!nt)
result.normalizationTooComplex = true;
else if (!normalizer->isInhabited(nt))
{
/* If the normalized upper bound we're mapping to a generic is
* uninhabited, then we must consider the subtyping relation not to
* hold.
*
* This happens eg in <T>() -> (T, T) <: () -> (string, number)
*
* T appears in covariant position and would have to be both string
* and number at once.
*
* No actual value is both a string and a number, so the test fails.
*
* TODO: We'll need to add explanitory context here.
*/
result.isSubtype = false;
}
result.andAlso(isCovariantWith(lowerBound, upperBound)); result.andAlso(isCovariantWith(lowerBound, upperBound));
} }

View file

@ -1044,6 +1044,14 @@ void persist(TypeId ty)
else if (get<GenericType>(t) || get<AnyType>(t) || get<FreeType>(t) || get<SingletonType>(t) || get<PrimitiveType>(t) || get<NegationType>(t)) else if (get<GenericType>(t) || get<AnyType>(t) || get<FreeType>(t) || get<SingletonType>(t) || get<PrimitiveType>(t) || get<NegationType>(t))
{ {
} }
else if (auto tfit = get<TypeFamilyInstanceType>(t))
{
for (auto ty : tfit->typeArguments)
queue.push_back(ty);
for (auto tp : tfit->packArguments)
persist(tp);
}
else else
{ {
LUAU_ASSERT(!"TypeId is not supported in a persist call"); LUAU_ASSERT(!"TypeId is not supported in a persist call");
@ -1072,6 +1080,14 @@ void persist(TypePackId tp)
else if (get<GenericTypePack>(tp)) else if (get<GenericTypePack>(tp))
{ {
} }
else if (auto tfitp = get<TypeFamilyInstanceTypePack>(tp))
{
for (auto ty : tfitp->typeArguments)
persist(ty);
for (auto tp : tfitp->packArguments)
persist(tp);
}
else else
{ {
LUAU_ASSERT(!"TypePackId is not supported in a persist call"); LUAU_ASSERT(!"TypePackId is not supported in a persist call");

View file

@ -17,7 +17,6 @@
#include "Luau/Type.h" #include "Luau/Type.h"
#include "Luau/TypePack.h" #include "Luau/TypePack.h"
#include "Luau/TypeUtils.h" #include "Luau/TypeUtils.h"
#include "Luau/Unifier.h"
#include "Luau/TypeFamily.h" #include "Luau/TypeFamily.h"
#include "Luau/VisitType.h" #include "Luau/VisitType.h"
@ -242,7 +241,8 @@ struct TypeChecker2
DenseHashSet<TypeId> noTypeFamilyErrors{nullptr}; DenseHashSet<TypeId> noTypeFamilyErrors{nullptr};
Normalizer normalizer; Normalizer normalizer;
Subtyping subtyping; Subtyping _subtyping;
NotNull<Subtyping> subtyping;
TypeChecker2(NotNull<BuiltinTypes> builtinTypes, NotNull<UnifierSharedState> unifierState, NotNull<TypeCheckLimits> limits, DcrLogger* logger, TypeChecker2(NotNull<BuiltinTypes> builtinTypes, NotNull<UnifierSharedState> unifierState, NotNull<TypeCheckLimits> limits, DcrLogger* logger,
const SourceModule* sourceModule, Module* module) const SourceModule* sourceModule, Module* module)
@ -253,7 +253,9 @@ struct TypeChecker2
, sourceModule(sourceModule) , sourceModule(sourceModule)
, module(module) , module(module)
, normalizer{&testArena, builtinTypes, unifierState, /* cacheInhabitance */ true} , normalizer{&testArena, builtinTypes, unifierState, /* cacheInhabitance */ true}
, subtyping{builtinTypes, NotNull{&testArena}, NotNull{&normalizer}, NotNull{unifierState->iceHandler}, NotNull{module->getModuleScope().get()}} , _subtyping{builtinTypes, NotNull{&testArena}, NotNull{&normalizer}, NotNull{unifierState->iceHandler},
NotNull{module->getModuleScope().get()}}
, subtyping(&_subtyping)
{ {
} }
@ -282,9 +284,9 @@ struct TypeChecker2
if (noTypeFamilyErrors.find(instance)) if (noTypeFamilyErrors.find(instance))
return instance; return instance;
TxnLog fake{}; ErrorVec errors = reduceFamilies(
ErrorVec errors = instance, location, TypeFamilyContext{NotNull{&testArena}, builtinTypes, stack.back(), NotNull{&normalizer}, ice, limits}, true)
reduceFamilies(instance, location, NotNull{&testArena}, builtinTypes, stack.back(), NotNull{&normalizer}, &fake, true).errors; .errors;
if (errors.empty()) if (errors.empty())
noTypeFamilyErrors.insert(instance); noTypeFamilyErrors.insert(instance);
@ -491,17 +493,7 @@ struct TypeChecker2
TypeArena* arena = &testArena; TypeArena* arena = &testArena;
TypePackId actualRetType = reconstructPack(ret->list, *arena); TypePackId actualRetType = reconstructPack(ret->list, *arena);
Unifier u{NotNull{&normalizer}, stack.back(), ret->location, Covariant}; testIsSubtype(actualRetType, expectedRetType, ret->location);
u.hideousFixMeGenericsAreActuallyFree = true;
u.tryUnify(actualRetType, expectedRetType);
const bool ok = (u.errors.empty() && u.log.empty()) || isErrorSuppressing(ret->location, actualRetType, ret->location, expectedRetType);
if (!ok)
{
for (const TypeError& e : u.errors)
reportError(e);
}
for (AstExpr* expr : ret->list) for (AstExpr* expr : ret->list)
visit(expr, ValueContext::RValue); visit(expr, ValueContext::RValue);
@ -532,9 +524,7 @@ struct TypeChecker2
TypeId annotationType = lookupAnnotation(var->annotation); TypeId annotationType = lookupAnnotation(var->annotation);
TypeId valueType = value ? lookupType(value) : nullptr; TypeId valueType = value ? lookupType(value) : nullptr;
if (valueType) if (valueType)
{ testIsSubtype(valueType, annotationType, value->location);
reportErrors(tryUnify(stack.back(), value->location, valueType, annotationType));
}
visit(var->annotation); visit(var->annotation);
} }
@ -559,7 +549,7 @@ struct TypeChecker2
if (var->annotation) if (var->annotation)
{ {
TypeId varType = lookupAnnotation(var->annotation); TypeId varType = lookupAnnotation(var->annotation);
reportErrors(tryUnify(stack.back(), value->location, valueTypes.head[j - i], varType)); testIsSubtype(valueTypes.head[j - i], varType, value->location);
visit(var->annotation); visit(var->annotation);
} }
@ -586,20 +576,20 @@ struct TypeChecker2
void visit(AstStatFor* forStatement) void visit(AstStatFor* forStatement)
{ {
NotNull<Scope> scope = stack.back();
if (forStatement->var->annotation) if (forStatement->var->annotation)
{ {
visit(forStatement->var->annotation); visit(forStatement->var->annotation);
reportErrors(tryUnify(scope, forStatement->var->location, builtinTypes->numberType, lookupAnnotation(forStatement->var->annotation)));
TypeId annotatedType = lookupAnnotation(forStatement->var->annotation);
testIsSubtype(builtinTypes->numberType, annotatedType, forStatement->var->location);
} }
auto checkNumber = [this, scope](AstExpr* expr) { auto checkNumber = [this](AstExpr* expr) {
if (!expr) if (!expr)
return; return;
visit(expr, ValueContext::RValue); visit(expr, ValueContext::RValue);
reportErrors(tryUnify(scope, expr->location, lookupType(expr), builtinTypes->numberType)); testIsSubtype(lookupType(expr), builtinTypes->numberType, expr->location);
}; };
checkNumber(forStatement->from); checkNumber(forStatement->from);
@ -689,8 +679,7 @@ struct TypeChecker2
} }
TypeId iteratorTy = follow(iteratorTypes.head[0]); TypeId iteratorTy = follow(iteratorTypes.head[0]);
auto checkFunction = [this, &arena, &scope, &forInStatement, &variableTypes]( auto checkFunction = [this, &arena, &forInStatement, &variableTypes](const FunctionType* iterFtv, std::vector<TypeId> iterTys, bool isMm) {
const FunctionType* iterFtv, std::vector<TypeId> iterTys, bool isMm) {
if (iterTys.size() < 1 || iterTys.size() > 3) if (iterTys.size() < 1 || iterTys.size() > 3)
{ {
if (isMm) if (isMm)
@ -713,7 +702,7 @@ struct TypeChecker2
} }
for (size_t i = 0; i < std::min(expectedVariableTypes.head.size(), variableTypes.size()); ++i) for (size_t i = 0; i < std::min(expectedVariableTypes.head.size(), variableTypes.size()); ++i)
reportErrors(tryUnify(scope, forInStatement->vars.data[i]->location, variableTypes[i], expectedVariableTypes.head[i])); testIsSubtype(variableTypes[i], expectedVariableTypes.head[i], forInStatement->vars.data[i]->location);
// nextFn is going to be invoked with (arrayTy, startIndexTy) // nextFn is going to be invoked with (arrayTy, startIndexTy)
@ -757,13 +746,13 @@ struct TypeChecker2
if (iterTys.size() >= 2 && flattenedArgTypes.head.size() > 0) if (iterTys.size() >= 2 && flattenedArgTypes.head.size() > 0)
{ {
size_t valueIndex = forInStatement->values.size > 1 ? 1 : 0; size_t valueIndex = forInStatement->values.size > 1 ? 1 : 0;
reportErrors(tryUnify(scope, forInStatement->values.data[valueIndex]->location, iterTys[1], flattenedArgTypes.head[0])); testIsSubtype(iterTys[1], flattenedArgTypes.head[0], forInStatement->values.data[valueIndex]->location);
} }
if (iterTys.size() == 3 && flattenedArgTypes.head.size() > 1) if (iterTys.size() == 3 && flattenedArgTypes.head.size() > 1)
{ {
size_t valueIndex = forInStatement->values.size > 2 ? 2 : 0; size_t valueIndex = forInStatement->values.size > 2 ? 2 : 0;
reportErrors(tryUnify(scope, forInStatement->values.data[valueIndex]->location, iterTys[2], flattenedArgTypes.head[1])); testIsSubtype(iterTys[2], flattenedArgTypes.head[1], forInStatement->values.data[valueIndex]->location);
} }
}; };
@ -795,9 +784,9 @@ struct TypeChecker2
{ {
if ((forInStatement->vars.size == 1 || forInStatement->vars.size == 2) && ttv->indexer) if ((forInStatement->vars.size == 1 || forInStatement->vars.size == 2) && ttv->indexer)
{ {
reportErrors(tryUnify(scope, forInStatement->vars.data[0]->location, variableTypes[0], ttv->indexer->indexType)); testIsSubtype(variableTypes[0], ttv->indexer->indexType, forInStatement->vars.data[0]->location);
if (variableTypes.size() == 2) if (variableTypes.size() == 2)
reportErrors(tryUnify(scope, forInStatement->vars.data[1]->location, variableTypes[1], ttv->indexer->indexResultType)); testIsSubtype(variableTypes[1], ttv->indexer->indexResultType, forInStatement->vars.data[1]->location);
} }
else else
reportError(GenericError{"Cannot iterate over a table without indexer"}, forInStatement->values.data[0]->location); reportError(GenericError{"Cannot iterate over a table without indexer"}, forInStatement->values.data[0]->location);
@ -820,7 +809,7 @@ struct TypeChecker2
if (const FunctionType* iterMmFtv = get<FunctionType>(*instantiatedIterMmTy)) if (const FunctionType* iterMmFtv = get<FunctionType>(*instantiatedIterMmTy))
{ {
TypePackId argPack = arena.addTypePack({iteratorTy}); TypePackId argPack = arena.addTypePack({iteratorTy});
reportErrors(tryUnify(scope, forInStatement->values.data[0]->location, argPack, iterMmFtv->argTypes)); testIsSubtype(argPack, iterMmFtv->argTypes, forInStatement->values.data[0]->location);
TypePack mmIteratorTypes = extendTypePack(arena, builtinTypes, iterMmFtv->retTypes, 3); TypePack mmIteratorTypes = extendTypePack(arena, builtinTypes, iterMmFtv->retTypes, 3);
@ -894,12 +883,7 @@ struct TypeChecker2
if (get<NeverType>(lhsType)) if (get<NeverType>(lhsType))
continue; continue;
testIsSubtype(rhsType, lhsType, rhs->location);
if (!isSubtype(rhsType, lhsType, stack.back()) &&
!isErrorSuppressing(assign->vars.data[i]->location, lhsType, assign->values.data[i]->location, rhsType))
{
reportError(TypeMismatch{lhsType, rhsType}, rhs->location);
}
} }
} }
@ -909,7 +893,7 @@ struct TypeChecker2
TypeId resultTy = visit(&fake, stat); TypeId resultTy = visit(&fake, stat);
TypeId varTy = lookupType(stat->var); TypeId varTy = lookupType(stat->var);
reportErrors(tryUnify(stack.back(), stat->location, resultTy, varTy)); testIsSubtype(resultTy, varTy, stat->location);
} }
void visit(AstStatFunction* stat) void visit(AstStatFunction* stat)
@ -1029,34 +1013,38 @@ struct TypeChecker2
void visit(AstExprConstantNil* expr) void visit(AstExprConstantNil* expr)
{ {
NotNull<Scope> scope = stack.back();
TypeId actualType = lookupType(expr); TypeId actualType = lookupType(expr);
TypeId expectedType = builtinTypes->nilType; TypeId expectedType = builtinTypes->nilType;
LUAU_ASSERT(isSubtype(actualType, expectedType, scope));
SubtypingResult r = subtyping->isSubtype(actualType, expectedType);
LUAU_ASSERT(r.isSubtype || r.isErrorSuppressing);
} }
void visit(AstExprConstantBool* expr) void visit(AstExprConstantBool* expr)
{ {
NotNull<Scope> scope = stack.back();
TypeId actualType = lookupType(expr); TypeId actualType = lookupType(expr);
TypeId expectedType = builtinTypes->booleanType; TypeId expectedType = builtinTypes->booleanType;
LUAU_ASSERT(isSubtype(actualType, expectedType, scope));
SubtypingResult r = subtyping->isSubtype(actualType, expectedType);
LUAU_ASSERT(r.isSubtype || r.isErrorSuppressing);
} }
void visit(AstExprConstantNumber* expr) void visit(AstExprConstantNumber* expr)
{ {
NotNull<Scope> scope = stack.back();
TypeId actualType = lookupType(expr); TypeId actualType = lookupType(expr);
TypeId expectedType = builtinTypes->numberType; TypeId expectedType = builtinTypes->numberType;
LUAU_ASSERT(isSubtype(actualType, expectedType, scope));
SubtypingResult r = subtyping->isSubtype(actualType, expectedType);
LUAU_ASSERT(r.isSubtype || r.isErrorSuppressing);
} }
void visit(AstExprConstantString* expr) void visit(AstExprConstantString* expr)
{ {
NotNull<Scope> scope = stack.back();
TypeId actualType = lookupType(expr); TypeId actualType = lookupType(expr);
TypeId expectedType = builtinTypes->stringType; TypeId expectedType = builtinTypes->stringType;
LUAU_ASSERT(isSubtype(actualType, expectedType, scope));
SubtypingResult r = subtyping->isSubtype(actualType, expectedType);
LUAU_ASSERT(r.isSubtype || r.isErrorSuppressing);
} }
void visit(AstExprLocal* expr) void visit(AstExprLocal* expr)
@ -1089,7 +1077,7 @@ struct TypeChecker2
TypeId fnTy = *originalCallTy; TypeId fnTy = *originalCallTy;
if (selectedOverloadTy) if (selectedOverloadTy)
{ {
SubtypingResult result = subtyping.isSubtype(*originalCallTy, *selectedOverloadTy); SubtypingResult result = subtyping->isSubtype(*originalCallTy, *selectedOverloadTy);
if (result.isSubtype) if (result.isSubtype)
fnTy = *selectedOverloadTy; fnTy = *selectedOverloadTy;
@ -1152,6 +1140,8 @@ struct TypeChecker2
NotNull{&normalizer}, NotNull{&normalizer},
NotNull{stack.back()}, NotNull{stack.back()},
ice, ice,
limits,
subtyping,
call->location, call->location,
}; };
@ -1243,6 +1233,8 @@ struct TypeChecker2
NotNull<Normalizer> normalizer; NotNull<Normalizer> normalizer;
NotNull<Scope> scope; NotNull<Scope> scope;
NotNull<InternalErrorReporter> ice; NotNull<InternalErrorReporter> ice;
NotNull<TypeCheckLimits> limits;
NotNull<Subtyping> subtyping;
Location callLoc; Location callLoc;
std::vector<TypeId> ok; std::vector<TypeId> ok;
@ -1252,32 +1244,38 @@ struct TypeChecker2
InsertionOrderedMap<TypeId, std::pair<Analysis, size_t>> resolution; InsertionOrderedMap<TypeId, std::pair<Analysis, size_t>> resolution;
private: private:
std::optional<ErrorVec> tryUnify(const Location& location, TypeId subTy, TypeId superTy, const LiteralProperties* literalProperties = nullptr) std::optional<ErrorVec> testIsSubtype(const Location& location, TypeId subTy, TypeId superTy)
{ {
Unifier u{normalizer, scope, location, Covariant}; auto r = subtyping->isSubtype(subTy, superTy);
u.ctx = CountMismatch::Arg; ErrorVec errors;
u.hideousFixMeGenericsAreActuallyFree = true;
u.enableNewSolver();
u.tryUnify(subTy, superTy, /*isFunctionCall*/ false, /*isIntersection*/ false, literalProperties);
if (u.errors.empty()) if (r.normalizationTooComplex)
errors.push_back(TypeError{location, NormalizationTooComplex{}});
if (!r.isSubtype && !r.isErrorSuppressing)
errors.push_back(TypeError{location, TypeMismatch{superTy, subTy}});
if (errors.empty())
return std::nullopt; return std::nullopt;
return std::move(u.errors); return errors;
} }
std::optional<ErrorVec> tryUnify(const Location& location, TypePackId subTy, TypePackId superTy) std::optional<ErrorVec> testIsSubtype(const Location& location, TypePackId subTy, TypePackId superTy)
{ {
Unifier u{normalizer, scope, location, Covariant}; auto r = subtyping->isSubtype(subTy, superTy);
u.ctx = CountMismatch::Arg; ErrorVec errors;
u.hideousFixMeGenericsAreActuallyFree = true;
u.enableNewSolver();
u.tryUnify(subTy, superTy);
if (u.errors.empty()) if (r.normalizationTooComplex)
errors.push_back(TypeError{location, NormalizationTooComplex{}});
if (!r.isSubtype && !r.isErrorSuppressing)
errors.push_back(TypeError{location, TypePackMismatch{superTy, subTy}});
if (errors.empty())
return std::nullopt; return std::nullopt;
return std::move(u.errors); return errors;
} }
std::pair<Analysis, ErrorVec> checkOverload( std::pair<Analysis, ErrorVec> checkOverload(
@ -1316,35 +1314,12 @@ struct TypeChecker2
expr->is<AstExprConstantString>() || expr->is<AstExprFunction>() || expr->is<AstExprTable>(); expr->is<AstExprConstantString>() || expr->is<AstExprFunction>() || expr->is<AstExprTable>();
} }
static std::unique_ptr<LiteralProperties> buildLiteralPropertiesSet(AstExpr* expr)
{
const AstExprTable* table = expr->as<AstExprTable>();
if (!table)
return nullptr;
std::unique_ptr<LiteralProperties> result = std::make_unique<LiteralProperties>(Name{});
for (const AstExprTable::Item& item : table->items)
{
if (item.kind != AstExprTable::Item::Record)
continue;
AstExprConstantString* keyExpr = item.key->as<AstExprConstantString>();
LUAU_ASSERT(keyExpr);
if (isLiteral(item.value))
result->insert(Name{keyExpr->value.begin(), keyExpr->value.end()});
}
return result;
}
LUAU_NOINLINE LUAU_NOINLINE
std::pair<Analysis, ErrorVec> checkOverload_( std::pair<Analysis, ErrorVec> checkOverload_(
TypeId fnTy, const FunctionType* fn, const TypePack* args, AstExpr* fnExpr, const std::vector<AstExpr*>* argExprs) TypeId fnTy, const FunctionType* fn, const TypePack* args, AstExpr* fnExpr, const std::vector<AstExpr*>* argExprs)
{ {
TxnLog fake; FamilyGraphReductionResult result =
FamilyGraphReductionResult result = reduceFamilies(fnTy, callLoc, arena, builtinTypes, scope, normalizer, &fake, /*force=*/true); reduceFamilies(fnTy, callLoc, TypeFamilyContext{arena, builtinTypes, scope, normalizer, ice, limits}, /*force=*/true);
if (!result.errors.empty()) if (!result.errors.empty())
return {OverloadIsNonviable, result.errors}; return {OverloadIsNonviable, result.errors};
@ -1363,9 +1338,7 @@ struct TypeChecker2
TypeId argTy = args->head[argOffset]; TypeId argTy = args->head[argOffset];
AstExpr* argLoc = argExprs->at(argOffset >= argExprs->size() ? argExprs->size() - 1 : argOffset); AstExpr* argLoc = argExprs->at(argOffset >= argExprs->size() ? argExprs->size() - 1 : argOffset);
std::unique_ptr<LiteralProperties> literalProperties{buildLiteralPropertiesSet(argLoc)}; if (auto errors = testIsSubtype(argLoc->location, argTy, paramTy))
if (auto errors = tryUnify(argLoc->location, argTy, paramTy, literalProperties.get()))
{ {
// Since we're stopping right here, we need to decide if this is a nonviable overload or if there is an arity mismatch. // Since we're stopping right here, we need to decide if this is a nonviable overload or if there is an arity mismatch.
// If it's a nonviable overload, then we need to keep going to get all type errors. // If it's a nonviable overload, then we need to keep going to get all type errors.
@ -1395,7 +1368,7 @@ struct TypeChecker2
} }
else if (auto vtp = get<VariadicTypePack>(follow(paramIter.tail()))) else if (auto vtp = get<VariadicTypePack>(follow(paramIter.tail())))
{ {
if (auto errors = tryUnify(argExpr->location, args->head[argOffset], vtp->ty)) if (auto errors = testIsSubtype(argExpr->location, args->head[argOffset], vtp->ty))
argumentErrors.insert(argumentErrors.end(), errors->begin(), errors->end()); argumentErrors.insert(argumentErrors.end(), errors->begin(), errors->end());
} }
else if (get<GenericTypePack>(follow(paramIter.tail()))) else if (get<GenericTypePack>(follow(paramIter.tail())))
@ -1414,7 +1387,7 @@ struct TypeChecker2
{ {
AstExpr* argExpr = argExprs->at(argExprs->size() - 1); AstExpr* argExpr = argExprs->at(argExprs->size() - 1);
if (auto errors = tryUnify(argExpr->location, vtp->ty, *paramIter)) if (auto errors = testIsSubtype(argExpr->location, vtp->ty, *paramIter))
argumentErrors.insert(argumentErrors.end(), errors->begin(), errors->end()); argumentErrors.insert(argumentErrors.end(), errors->begin(), errors->end());
} }
else if (!isOptional(*paramIter)) else if (!isOptional(*paramIter))
@ -1439,7 +1412,7 @@ struct TypeChecker2
if (paramIter.tail() && args->tail) if (paramIter.tail() && args->tail)
{ {
if (auto errors = tryUnify(argLoc, *args->tail, *paramIter.tail())) if (auto errors = testIsSubtype(argLoc, *args->tail, *paramIter.tail()))
argumentErrors.insert(argumentErrors.end(), errors->begin(), errors->end()); argumentErrors.insert(argumentErrors.end(), errors->begin(), errors->end());
} }
else if (paramIter.tail()) else if (paramIter.tail())
@ -1604,20 +1577,18 @@ struct TypeChecker2
visit(indexExpr->expr, ValueContext::RValue); visit(indexExpr->expr, ValueContext::RValue);
visit(indexExpr->index, ValueContext::RValue); visit(indexExpr->index, ValueContext::RValue);
NotNull<Scope> scope = stack.back();
TypeId exprType = lookupType(indexExpr->expr); TypeId exprType = lookupType(indexExpr->expr);
TypeId indexType = lookupType(indexExpr->index); TypeId indexType = lookupType(indexExpr->index);
if (auto tt = get<TableType>(exprType)) if (auto tt = get<TableType>(exprType))
{ {
if (tt->indexer) if (tt->indexer)
reportErrors(tryUnify(scope, indexExpr->index->location, indexType, tt->indexer->indexType)); testIsSubtype(indexType, tt->indexer->indexType, indexExpr->index->location);
else else
reportError(CannotExtendTable{exprType, CannotExtendTable::Indexer, "indexer??"}, indexExpr->location); reportError(CannotExtendTable{exprType, CannotExtendTable::Indexer, "indexer??"}, indexExpr->location);
} }
else if (auto cls = get<ClassType>(exprType); cls && cls->indexer) else if (auto cls = get<ClassType>(exprType); cls && cls->indexer)
reportErrors(tryUnify(scope, indexExpr->index->location, indexType, cls->indexer->indexType)); testIsSubtype(indexType, cls->indexer->indexType, indexExpr->index->location);
else if (get<UnionType>(exprType) && isOptional(exprType)) else if (get<UnionType>(exprType) && isOptional(exprType))
reportError(OptionalValueAccess{exprType}, indexExpr->location); reportError(OptionalValueAccess{exprType}, indexExpr->location);
} }
@ -1668,11 +1639,7 @@ struct TypeChecker2
TypeId inferredArgTy = *argIt; TypeId inferredArgTy = *argIt;
TypeId annotatedArgTy = lookupAnnotation(arg->annotation); TypeId annotatedArgTy = lookupAnnotation(arg->annotation);
if (!isSubtype(inferredArgTy, annotatedArgTy, stack.back()) && testIsSubtype(inferredArgTy, annotatedArgTy, arg->location);
!isErrorSuppressing(arg->location, inferredArgTy, arg->annotation->location, annotatedArgTy))
{
reportError(TypeMismatch{inferredArgTy, annotatedArgTy}, arg->location);
}
} }
++argIt; ++argIt;
@ -1699,7 +1666,6 @@ struct TypeChecker2
{ {
visit(expr->expr, ValueContext::RValue); visit(expr->expr, ValueContext::RValue);
NotNull<Scope> scope = stack.back();
TypeId operandType = lookupType(expr->expr); TypeId operandType = lookupType(expr->expr);
TypeId resultType = lookupType(expr); TypeId resultType = lookupType(expr);
@ -1717,7 +1683,7 @@ struct TypeChecker2
{ {
if (expr->op == AstExprUnary::Op::Len) if (expr->op == AstExprUnary::Op::Len)
{ {
reportErrors(tryUnify(scope, expr->location, follow(*ret), builtinTypes->numberType)); testIsSubtype(follow(*ret), builtinTypes->numberType, expr->location);
} }
} }
else else
@ -1737,12 +1703,9 @@ struct TypeChecker2
TypeId expectedFunction = testArena.addType(FunctionType{expectedArgs, expectedRet}); TypeId expectedFunction = testArena.addType(FunctionType{expectedArgs, expectedRet});
ErrorVec errors = tryUnify(scope, expr->location, *mm, expectedFunction); bool success = testIsSubtype(*mm, expectedFunction, expr->location);
if (!errors.empty() && !isErrorSuppressing(expr->expr->location, *firstArg, expr->expr->location, operandType)) if (!success)
{
reportError(TypeMismatch{*firstArg, operandType}, expr->location);
return; return;
}
} }
return; return;
@ -1768,7 +1731,7 @@ struct TypeChecker2
} }
else if (expr->op == AstExprUnary::Op::Minus) else if (expr->op == AstExprUnary::Op::Minus)
{ {
reportErrors(tryUnify(scope, expr->location, operandType, builtinTypes->numberType)); testIsSubtype(operandType, builtinTypes->numberType, expr->location);
} }
else if (expr->op == AstExprUnary::Op::Not) else if (expr->op == AstExprUnary::Op::Not)
{ {
@ -1792,7 +1755,7 @@ struct TypeChecker2
TypeId leftType = lookupType(expr->left); TypeId leftType = lookupType(expr->left);
TypeId rightType = lookupType(expr->right); TypeId rightType = lookupType(expr->right);
TypeId expectedResult = lookupType(expr); TypeId expectedResult = follow(lookupType(expr));
if (get<TypeFamilyInstanceType>(expectedResult)) if (get<TypeFamilyInstanceType>(expectedResult))
{ {
@ -1928,7 +1891,7 @@ struct TypeChecker2
TypeId expectedTy = testArena.addType(FunctionType(expectedArgs, expectedRets)); TypeId expectedTy = testArena.addType(FunctionType(expectedArgs, expectedRets));
reportErrors(tryUnify(scope, expr->location, follow(*mm), expectedTy)); testIsSubtype(follow(*mm), expectedTy, expr->location);
std::optional<TypeId> ret = first(ftv->retTypes); std::optional<TypeId> ret = first(ftv->retTypes);
if (ret) if (ret)
@ -2022,13 +1985,13 @@ struct TypeChecker2
case AstExprBinary::Op::Mod: case AstExprBinary::Op::Mod:
LUAU_ASSERT(FFlag::LuauFloorDivision || expr->op != AstExprBinary::Op::FloorDiv); LUAU_ASSERT(FFlag::LuauFloorDivision || expr->op != AstExprBinary::Op::FloorDiv);
reportErrors(tryUnify(scope, expr->left->location, leftType, builtinTypes->numberType)); testIsSubtype(leftType, builtinTypes->numberType, expr->left->location);
reportErrors(tryUnify(scope, expr->right->location, rightType, builtinTypes->numberType)); testIsSubtype(rightType, builtinTypes->numberType, expr->right->location);
return builtinTypes->numberType; return builtinTypes->numberType;
case AstExprBinary::Op::Concat: case AstExprBinary::Op::Concat:
reportErrors(tryUnify(scope, expr->left->location, leftType, builtinTypes->stringType)); testIsSubtype(leftType, builtinTypes->stringType, expr->left->location);
reportErrors(tryUnify(scope, expr->right->location, rightType, builtinTypes->stringType)); testIsSubtype(rightType, builtinTypes->stringType, expr->right->location);
return builtinTypes->stringType; return builtinTypes->stringType;
case AstExprBinary::Op::CompareGe: case AstExprBinary::Op::CompareGe:
@ -2041,12 +2004,12 @@ struct TypeChecker2
if (normLeft && normLeft->isExactlyNumber()) if (normLeft && normLeft->isExactlyNumber())
{ {
reportErrors(tryUnify(scope, expr->right->location, rightType, builtinTypes->numberType)); testIsSubtype(rightType, builtinTypes->numberType, expr->right->location);
return builtinTypes->numberType; return builtinTypes->numberType;
} }
else if (normLeft && normLeft->isSubtypeOfString()) else if (normLeft && normLeft->isSubtypeOfString())
{ {
reportErrors(tryUnify(scope, expr->right->location, rightType, builtinTypes->stringType)); testIsSubtype(rightType, builtinTypes->stringType, expr->right->location);
return builtinTypes->stringType; return builtinTypes->stringType;
} }
else else
@ -2081,10 +2044,10 @@ struct TypeChecker2
TypeId computedType = lookupType(expr->expr); TypeId computedType = lookupType(expr->expr);
// Note: As an optimization, we try 'number <: number | string' first, as that is the more likely case. // Note: As an optimization, we try 'number <: number | string' first, as that is the more likely case.
if (isSubtype(annotationType, computedType, stack.back(), true)) if (auto r = subtyping->isSubtype(annotationType, computedType); r.isSubtype || r.isErrorSuppressing)
return; return;
if (isSubtype(computedType, annotationType, stack.back(), true)) if (auto r = subtyping->isSubtype(computedType, annotationType); r.isSubtype || r.isErrorSuppressing)
return; return;
reportError(TypesAreUnrelated{computedType, annotationType}, expr->location); reportError(TypesAreUnrelated{computedType, annotationType}, expr->location);
@ -2424,33 +2387,30 @@ struct TypeChecker2
} }
} }
template<typename TID> bool testIsSubtype(TypeId subTy, TypeId superTy, Location location)
bool isSubtype(TID subTy, TID superTy, NotNull<Scope> scope, bool genericsOkay = false)
{ {
TypeArena arena; SubtypingResult r = subtyping->isSubtype(subTy, superTy);
Unifier u{NotNull{&normalizer}, scope, Location{}, Covariant};
u.hideousFixMeGenericsAreActuallyFree = genericsOkay;
u.enableNewSolver();
u.tryUnify(subTy, superTy); if (r.normalizationTooComplex)
const bool ok = u.errors.empty() && u.log.empty(); reportError(NormalizationTooComplex{}, location);
return ok;
if (!r.isSubtype && !r.isErrorSuppressing)
reportError(TypeMismatch{superTy, subTy}, location);
return r.isSubtype;
} }
template<typename TID> bool testIsSubtype(TypePackId subTy, TypePackId superTy, Location location)
ErrorVec tryUnify(NotNull<Scope> scope, const Location& location, TID subTy, TID superTy, CountMismatch::Context context = CountMismatch::Arg,
bool genericsOkay = false)
{ {
Unifier u{NotNull{&normalizer}, scope, location, Covariant}; SubtypingResult r = subtyping->isSubtype(subTy, superTy);
u.ctx = context;
u.hideousFixMeGenericsAreActuallyFree = genericsOkay;
u.enableNewSolver();
u.tryUnify(subTy, superTy);
if (isErrorSuppressing(location, subTy, location, superTy)) if (r.normalizationTooComplex)
return {}; reportError(NormalizationTooComplex{}, location);
return std::move(u.errors); if (!r.isSubtype && !r.isErrorSuppressing)
reportError(TypePackMismatch{superTy, subTy}, location);
return r.isSubtype;
} }
void reportError(TypeErrorData data, const Location& location) void reportError(TypeErrorData data, const Location& location)

View file

@ -6,12 +6,13 @@
#include "Luau/DenseHash.h" #include "Luau/DenseHash.h"
#include "Luau/Instantiation.h" #include "Luau/Instantiation.h"
#include "Luau/Normalize.h" #include "Luau/Normalize.h"
#include "Luau/Simplify.h"
#include "Luau/Substitution.h" #include "Luau/Substitution.h"
#include "Luau/ToString.h" #include "Luau/ToString.h"
#include "Luau/TxnLog.h" #include "Luau/TxnLog.h"
#include "Luau/TypeCheckLimits.h" #include "Luau/TypeCheckLimits.h"
#include "Luau/TypeUtils.h" #include "Luau/TypeUtils.h"
#include "Luau/Unifier.h" #include "Luau/Unifier2.h"
#include "Luau/VisitType.h" #include "Luau/VisitType.h"
LUAU_DYNAMIC_FASTINTVARIABLE(LuauTypeFamilyGraphReductionMaximumSteps, 1'000'000); LUAU_DYNAMIC_FASTINTVARIABLE(LuauTypeFamilyGraphReductionMaximumSteps, 1'000'000);
@ -56,38 +57,23 @@ struct InstanceCollector : TypeOnceVisitor
struct FamilyReducer struct FamilyReducer
{ {
ConstraintSolver* solver = nullptr; TypeFamilyContext ctx;
// Conditionally from the solver if one is provided.
NotNull<TypeArena> arena;
NotNull<BuiltinTypes> builtins;
NotNull<Normalizer> normalizer;
std::deque<TypeId> queuedTys; std::deque<TypeId> queuedTys;
std::deque<TypePackId> queuedTps; std::deque<TypePackId> queuedTps;
DenseHashSet<const void*> irreducible{nullptr}; DenseHashSet<const void*> irreducible{nullptr};
FamilyGraphReductionResult result; FamilyGraphReductionResult result;
TxnLog* parentLog = nullptr;
TxnLog log;
bool force = false; bool force = false;
// Local to the constraint being reduced. // Local to the constraint being reduced.
Location location; Location location;
NotNull<Scope> scope;
FamilyReducer(std::deque<TypeId> queuedTys, std::deque<TypePackId> queuedTps, Location location, NotNull<TypeArena> arena, FamilyReducer(std::deque<TypeId> queuedTys, std::deque<TypePackId> queuedTps, Location location, TypeFamilyContext ctx, bool force = false)
NotNull<BuiltinTypes> builtins, NotNull<Scope> scope, NotNull<Normalizer> normalizer, ConstraintSolver* solver, TxnLog* parentLog = nullptr, bool force = false) : ctx(ctx)
: solver(solver)
, arena(arena)
, builtins(builtins)
, normalizer(normalizer)
, queuedTys(std::move(queuedTys)) , queuedTys(std::move(queuedTys))
, queuedTps(std::move(queuedTps)) , queuedTps(std::move(queuedTps))
, parentLog(parentLog)
, log(parentLog)
, force(force) , force(force)
, location(location) , location(location)
, scope(scope)
{ {
} }
@ -100,16 +86,16 @@ struct FamilyReducer
SkipTestResult testForSkippability(TypeId ty) SkipTestResult testForSkippability(TypeId ty)
{ {
ty = log.follow(ty); ty = follow(ty);
if (log.is<TypeFamilyInstanceType>(ty)) if (is<TypeFamilyInstanceType>(ty))
{ {
if (!irreducible.contains(ty)) if (!irreducible.contains(ty))
return SkipTestResult::Defer; return SkipTestResult::Defer;
else else
return SkipTestResult::Irreducible; return SkipTestResult::Irreducible;
} }
else if (log.is<GenericType>(ty)) else if (is<GenericType>(ty))
{ {
return SkipTestResult::Irreducible; return SkipTestResult::Irreducible;
} }
@ -119,16 +105,16 @@ struct FamilyReducer
SkipTestResult testForSkippability(TypePackId ty) SkipTestResult testForSkippability(TypePackId ty)
{ {
ty = log.follow(ty); ty = follow(ty);
if (log.is<TypeFamilyInstanceTypePack>(ty)) if (is<TypeFamilyInstanceTypePack>(ty))
{ {
if (!irreducible.contains(ty)) if (!irreducible.contains(ty))
return SkipTestResult::Defer; return SkipTestResult::Defer;
else else
return SkipTestResult::Irreducible; return SkipTestResult::Irreducible;
} }
else if (log.is<GenericTypePack>(ty)) else if (is<GenericTypePack>(ty))
{ {
return SkipTestResult::Irreducible; return SkipTestResult::Irreducible;
} }
@ -139,10 +125,7 @@ struct FamilyReducer
template<typename T> template<typename T>
void replace(T subject, T replacement) void replace(T subject, T replacement)
{ {
if (parentLog) asMutable(subject)->ty.template emplace<Unifiable::Bound<T>>(replacement);
parentLog->replace(subject, Unifiable::Bound{replacement});
else
asMutable(subject)->ty.template emplace<Unifiable::Bound<T>>(replacement);
if constexpr (std::is_same_v<T, TypeId>) if constexpr (std::is_same_v<T, TypeId>)
result.reducedTypes.insert(subject); result.reducedTypes.insert(subject);
@ -230,38 +213,36 @@ struct FamilyReducer
void stepType() void stepType()
{ {
TypeId subject = log.follow(queuedTys.front()); TypeId subject = follow(queuedTys.front());
queuedTys.pop_front(); queuedTys.pop_front();
if (irreducible.contains(subject)) if (irreducible.contains(subject))
return; return;
if (const TypeFamilyInstanceType* tfit = log.get<TypeFamilyInstanceType>(subject)) if (const TypeFamilyInstanceType* tfit = get<TypeFamilyInstanceType>(subject))
{ {
if (!testParameters(subject, tfit)) if (!testParameters(subject, tfit))
return; return;
TypeFamilyReductionResult<TypeId> result = TypeFamilyReductionResult<TypeId> result = tfit->family->reducer(tfit->typeArguments, tfit->packArguments, NotNull{&ctx});
tfit->family->reducer(tfit->typeArguments, tfit->packArguments, arena, builtins, NotNull{&log}, scope, normalizer, solver);
handleFamilyReduction(subject, result); handleFamilyReduction(subject, result);
} }
} }
void stepPack() void stepPack()
{ {
TypePackId subject = log.follow(queuedTps.front()); TypePackId subject = follow(queuedTps.front());
queuedTps.pop_front(); queuedTps.pop_front();
if (irreducible.contains(subject)) if (irreducible.contains(subject))
return; return;
if (const TypeFamilyInstanceTypePack* tfit = log.get<TypeFamilyInstanceTypePack>(subject)) if (const TypeFamilyInstanceTypePack* tfit = get<TypeFamilyInstanceTypePack>(subject))
{ {
if (!testParameters(subject, tfit)) if (!testParameters(subject, tfit))
return; return;
TypeFamilyReductionResult<TypePackId> result = TypeFamilyReductionResult<TypePackId> result = tfit->family->reducer(tfit->typeArguments, tfit->packArguments, NotNull{&ctx});
tfit->family->reducer(tfit->typeArguments, tfit->packArguments, arena, builtins, NotNull{&log}, scope, normalizer, solver);
handleFamilyReduction(subject, result); handleFamilyReduction(subject, result);
} }
} }
@ -275,11 +256,10 @@ struct FamilyReducer
} }
}; };
static FamilyGraphReductionResult reduceFamiliesInternal(std::deque<TypeId> queuedTys, std::deque<TypePackId> queuedTps, Location location, static FamilyGraphReductionResult reduceFamiliesInternal(
NotNull<TypeArena> arena, NotNull<BuiltinTypes> builtins, NotNull<Scope> scope, NotNull<Normalizer> normalizer, ConstraintSolver* solver, std::deque<TypeId> queuedTys, std::deque<TypePackId> queuedTps, Location location, TypeFamilyContext ctx, bool force)
TxnLog* log, bool force)
{ {
FamilyReducer reducer{std::move(queuedTys), std::move(queuedTps), location, arena, builtins, scope, normalizer, solver, log, force}; FamilyReducer reducer{std::move(queuedTys), std::move(queuedTps), location, ctx, force};
int iterationCount = 0; int iterationCount = 0;
while (!reducer.done()) while (!reducer.done())
@ -297,8 +277,7 @@ static FamilyGraphReductionResult reduceFamiliesInternal(std::deque<TypeId> queu
return std::move(reducer.result); return std::move(reducer.result);
} }
FamilyGraphReductionResult reduceFamilies(TypeId entrypoint, Location location, NotNull<TypeArena> arena, NotNull<BuiltinTypes> builtins, FamilyGraphReductionResult reduceFamilies(TypeId entrypoint, Location location, TypeFamilyContext ctx, bool force)
NotNull<Scope> scope, NotNull<Normalizer> normalizer, TxnLog* log, bool force)
{ {
InstanceCollector collector; InstanceCollector collector;
@ -314,11 +293,10 @@ FamilyGraphReductionResult reduceFamilies(TypeId entrypoint, Location location,
if (collector.tys.empty() && collector.tps.empty()) if (collector.tys.empty() && collector.tps.empty())
return {}; return {};
return reduceFamiliesInternal(std::move(collector.tys), std::move(collector.tps), location, arena, builtins, scope, normalizer, nullptr, log, force); return reduceFamiliesInternal(std::move(collector.tys), std::move(collector.tps), location, ctx, force);
} }
FamilyGraphReductionResult reduceFamilies(TypePackId entrypoint, Location location, NotNull<TypeArena> arena, NotNull<BuiltinTypes> builtins, FamilyGraphReductionResult reduceFamilies(TypePackId entrypoint, Location location, TypeFamilyContext ctx, bool force)
NotNull<Scope> scope, NotNull<Normalizer> normalizer, TxnLog* log, bool force)
{ {
InstanceCollector collector; InstanceCollector collector;
@ -334,94 +312,52 @@ FamilyGraphReductionResult reduceFamilies(TypePackId entrypoint, Location locati
if (collector.tys.empty() && collector.tps.empty()) if (collector.tys.empty() && collector.tps.empty())
return {}; return {};
return reduceFamiliesInternal(std::move(collector.tys), std::move(collector.tps), location, arena, builtins, scope, normalizer, nullptr, log, force); return reduceFamiliesInternal(std::move(collector.tys), std::move(collector.tps), location, ctx, force);
} }
FamilyGraphReductionResult reduceFamilies( bool isPending(TypeId ty, ConstraintSolver* solver)
NotNull<ConstraintSolver> solver, TypeId entrypoint, Location location, NotNull<Scope> scope, TxnLog* log, bool force)
{ {
InstanceCollector collector; return is<BlockedType>(ty) || is<PendingExpansionType>(ty) || is<TypeFamilyInstanceType>(ty) || (solver && solver->hasUnresolvedConstraints(ty));
try
{
collector.traverse(entrypoint);
}
catch (RecursionLimitException&)
{
return FamilyGraphReductionResult{};
}
if (collector.tys.empty() && collector.tps.empty())
return {};
return reduceFamiliesInternal(std::move(collector.tys), std::move(collector.tps), location, solver->arena, solver->builtinTypes, scope, solver->normalizer, solver.get(), log, force);
} }
FamilyGraphReductionResult reduceFamilies( TypeFamilyReductionResult<TypeId> numericBinopFamilyFn(
NotNull<ConstraintSolver> solver, TypePackId entrypoint, Location location, NotNull<Scope> scope, TxnLog* log, bool force) std::vector<TypeId> typeParams, std::vector<TypePackId> packParams, NotNull<TypeFamilyContext> ctx, const std::string metamethod)
{
InstanceCollector collector;
try
{
collector.traverse(entrypoint);
}
catch (RecursionLimitException&)
{
return FamilyGraphReductionResult{};
}
if (collector.tys.empty() && collector.tps.empty())
return {};
return reduceFamiliesInternal(std::move(collector.tys), std::move(collector.tps), location, solver->arena, solver->builtinTypes, scope, solver->normalizer, solver.get(), log, force);
}
bool isPending(TypeId ty, NotNull<TxnLog> log, ConstraintSolver* solver)
{
return log->is<BlockedType>(ty) || log->is<PendingExpansionType>(ty) || log->is<TypeFamilyInstanceType>(ty)
|| (solver && solver->hasUnresolvedConstraints(ty));
}
TypeFamilyReductionResult<TypeId> addFamilyFn(std::vector<TypeId> typeParams, std::vector<TypePackId> packParams, NotNull<TypeArena> arena,
NotNull<BuiltinTypes> builtins, NotNull<TxnLog> log, NotNull<Scope> scope, NotNull<Normalizer> normalizer, ConstraintSolver* solver)
{ {
if (typeParams.size() != 2 || !packParams.empty()) if (typeParams.size() != 2 || !packParams.empty())
{ {
// TODO: ICE? ctx->ice->ice("encountered a type family instance without the required argument structure");
LUAU_ASSERT(false); LUAU_ASSERT(false);
return {std::nullopt, true, {}, {}};
} }
TypeId lhsTy = log->follow(typeParams.at(0)); TypeId lhsTy = follow(typeParams.at(0));
TypeId rhsTy = log->follow(typeParams.at(1)); TypeId rhsTy = follow(typeParams.at(1));
const NormalizedType* normLhsTy = normalizer->normalize(lhsTy); const NormalizedType* normLhsTy = ctx->normalizer->normalize(lhsTy);
const NormalizedType* normRhsTy = normalizer->normalize(rhsTy); const NormalizedType* normRhsTy = ctx->normalizer->normalize(rhsTy);
if (!normLhsTy || !normRhsTy) if (!normLhsTy || !normRhsTy)
{ {
return {std::nullopt, false, {}, {}}; return {std::nullopt, false, {}, {}};
} }
else if (log->is<AnyType>(normLhsTy->tops) || log->is<AnyType>(normRhsTy->tops)) else if (is<AnyType>(normLhsTy->tops) || is<AnyType>(normRhsTy->tops))
{ {
return {builtins->anyType, false, {}, {}}; return {ctx->builtins->anyType, false, {}, {}};
} }
else if ((normLhsTy->hasNumbers() || normLhsTy->hasTops()) && (normRhsTy->hasNumbers() || normRhsTy->hasTops())) else if ((normLhsTy->hasNumbers() || normLhsTy->hasTops()) && (normRhsTy->hasNumbers() || normRhsTy->hasTops()))
{ {
return {builtins->numberType, false, {}, {}}; return {ctx->builtins->numberType, false, {}, {}};
} }
else if (log->is<ErrorType>(lhsTy) || log->is<ErrorType>(rhsTy)) else if (is<ErrorType>(lhsTy) || is<ErrorType>(rhsTy))
{ {
return {builtins->errorRecoveryType(), false, {}, {}}; return {ctx->builtins->errorRecoveryType(), false, {}, {}};
} }
else if (log->is<NeverType>(lhsTy) || log->is<NeverType>(rhsTy)) else if (is<NeverType>(lhsTy) || is<NeverType>(rhsTy))
{ {
return {builtins->neverType, false, {}, {}}; return {ctx->builtins->neverType, false, {}, {}};
} }
else if (isPending(lhsTy, log, solver)) else if (isPending(lhsTy, ctx->solver))
{ {
return {std::nullopt, false, {lhsTy}, {}}; return {std::nullopt, false, {lhsTy}, {}};
} }
else if (isPending(rhsTy, log, solver)) else if (isPending(rhsTy, ctx->solver))
{ {
return {std::nullopt, false, {rhsTy}, {}}; return {std::nullopt, false, {rhsTy}, {}};
} }
@ -430,28 +366,28 @@ TypeFamilyReductionResult<TypeId> addFamilyFn(std::vector<TypeId> typeParams, st
// the necessary state to do that, even if we intend to just eat the errors. // the necessary state to do that, even if we intend to just eat the errors.
ErrorVec dummy; ErrorVec dummy;
std::optional<TypeId> addMm = findMetatableEntry(builtins, dummy, lhsTy, "__add", Location{}); std::optional<TypeId> mmType = findMetatableEntry(ctx->builtins, dummy, lhsTy, metamethod, Location{});
bool reversed = false; bool reversed = false;
if (!addMm) if (!mmType)
{ {
addMm = findMetatableEntry(builtins, dummy, rhsTy, "__add", Location{}); mmType = findMetatableEntry(ctx->builtins, dummy, rhsTy, metamethod, Location{});
reversed = true; reversed = true;
} }
if (!addMm) if (!mmType)
return {std::nullopt, true, {}, {}}; return {std::nullopt, true, {}, {}};
if (isPending(log->follow(*addMm), log, solver)) mmType = follow(*mmType);
return {std::nullopt, false, {log->follow(*addMm)}, {}}; if (isPending(*mmType, ctx->solver))
return {std::nullopt, false, {*mmType}, {}};
const FunctionType* mmFtv = log->get<FunctionType>(log->follow(*addMm)); const FunctionType* mmFtv = get<FunctionType>(*mmType);
if (!mmFtv) if (!mmFtv)
return {std::nullopt, true, {}, {}}; return {std::nullopt, true, {}, {}};
TypeCheckLimits limits; // TODO: We need to thread TypeCheckLimits in from Frontend to here. if (std::optional<TypeId> instantiatedMmType = instantiate(ctx->builtins, ctx->arena, ctx->limits, ctx->scope, *mmType))
if (std::optional<TypeId> instantiatedAddMm = instantiate(builtins, arena, NotNull{&limits}, scope, log->follow(*addMm)))
{ {
if (const FunctionType* instantiatedMmFtv = get<FunctionType>(*instantiatedAddMm)) if (const FunctionType* instantiatedMmFtv = get<FunctionType>(*instantiatedMmType))
{ {
std::vector<TypeId> inferredArgs; std::vector<TypeId> inferredArgs;
if (!reversed) if (!reversed)
@ -459,22 +395,19 @@ TypeFamilyReductionResult<TypeId> addFamilyFn(std::vector<TypeId> typeParams, st
else else
inferredArgs = {rhsTy, lhsTy}; inferredArgs = {rhsTy, lhsTy};
TypePackId inferredArgPack = arena->addTypePack(std::move(inferredArgs)); TypePackId inferredArgPack = ctx->arena->addTypePack(std::move(inferredArgs));
Unifier u{normalizer, scope, Location{}, Variance::Covariant, log.get()}; Unifier2 u2{ctx->arena, ctx->builtins, ctx->scope, ctx->ice};
u.tryUnify(inferredArgPack, instantiatedMmFtv->argTypes); if (!u2.unify(inferredArgPack, instantiatedMmFtv->argTypes))
return {std::nullopt, true, {}, {}}; // occurs check failed
if (std::optional<TypeId> ret = first(instantiatedMmFtv->retTypes); ret && u.errors.empty()) if (std::optional<TypeId> ret = first(instantiatedMmFtv->retTypes))
{ return {*ret, false, {}, {}};
return {u.log.follow(*ret), false, {}, {}};
}
else else
{
return {std::nullopt, true, {}, {}}; return {std::nullopt, true, {}, {}};
}
} }
else else
{ {
return {builtins->errorRecoveryType(), false, {}, {}}; return {ctx->builtins->errorRecoveryType(), false, {}, {}};
} }
} }
else else
@ -484,9 +417,171 @@ TypeFamilyReductionResult<TypeId> addFamilyFn(std::vector<TypeId> typeParams, st
} }
} }
TypeFamilyReductionResult<TypeId> addFamilyFn(std::vector<TypeId> typeParams, std::vector<TypePackId> packParams, NotNull<TypeFamilyContext> ctx)
{
if (typeParams.size() != 2 || !packParams.empty())
{
ctx->ice->ice("add type family: encountered a type family instance without the required argument structure");
LUAU_ASSERT(false);
}
return numericBinopFamilyFn(typeParams, packParams, ctx, "__add");
}
TypeFamilyReductionResult<TypeId> subFamilyFn(std::vector<TypeId> typeParams, std::vector<TypePackId> packParams, NotNull<TypeFamilyContext> ctx)
{
if (typeParams.size() != 2 || !packParams.empty())
{
ctx->ice->ice("sub type family: encountered a type family instance without the required argument structure");
LUAU_ASSERT(false);
}
return numericBinopFamilyFn(typeParams, packParams, ctx, "__sub");
}
TypeFamilyReductionResult<TypeId> mulFamilyFn(std::vector<TypeId> typeParams, std::vector<TypePackId> packParams, NotNull<TypeFamilyContext> ctx)
{
if (typeParams.size() != 2 || !packParams.empty())
{
ctx->ice->ice("mul type family: encountered a type family instance without the required argument structure");
LUAU_ASSERT(false);
}
return numericBinopFamilyFn(typeParams, packParams, ctx, "__mul");
}
TypeFamilyReductionResult<TypeId> divFamilyFn(std::vector<TypeId> typeParams, std::vector<TypePackId> packParams, NotNull<TypeFamilyContext> ctx)
{
if (typeParams.size() != 2 || !packParams.empty())
{
ctx->ice->ice("div type family: encountered a type family instance without the required argument structure");
LUAU_ASSERT(false);
}
return numericBinopFamilyFn(typeParams, packParams, ctx, "__div");
}
TypeFamilyReductionResult<TypeId> idivFamilyFn(std::vector<TypeId> typeParams, std::vector<TypePackId> packParams, NotNull<TypeFamilyContext> ctx)
{
if (typeParams.size() != 2 || !packParams.empty())
{
ctx->ice->ice("integer div type family: encountered a type family instance without the required argument structure");
LUAU_ASSERT(false);
}
return numericBinopFamilyFn(typeParams, packParams, ctx, "__idiv");
}
TypeFamilyReductionResult<TypeId> powFamilyFn(std::vector<TypeId> typeParams, std::vector<TypePackId> packParams, NotNull<TypeFamilyContext> ctx)
{
if (typeParams.size() != 2 || !packParams.empty())
{
ctx->ice->ice("pow type family: encountered a type family instance without the required argument structure");
LUAU_ASSERT(false);
}
return numericBinopFamilyFn(typeParams, packParams, ctx, "__pow");
}
TypeFamilyReductionResult<TypeId> modFamilyFn(std::vector<TypeId> typeParams, std::vector<TypePackId> packParams, NotNull<TypeFamilyContext> ctx)
{
if (typeParams.size() != 2 || !packParams.empty())
{
ctx->ice->ice("modulo type family: encountered a type family instance without the required argument structure");
LUAU_ASSERT(false);
}
return numericBinopFamilyFn(typeParams, packParams, ctx, "__mod");
}
TypeFamilyReductionResult<TypeId> andFamilyFn(std::vector<TypeId> typeParams, std::vector<TypePackId> packParams, NotNull<TypeFamilyContext> ctx)
{
if (typeParams.size() != 2 || !packParams.empty())
{
ctx->ice->ice("and type family: encountered a type family instance without the required argument structure");
LUAU_ASSERT(false);
}
TypeId lhsTy = follow(typeParams.at(0));
TypeId rhsTy = follow(typeParams.at(1));
if (isPending(lhsTy, ctx->solver))
{
return {std::nullopt, false, {lhsTy}, {}};
}
else if (isPending(rhsTy, ctx->solver))
{
return {std::nullopt, false, {rhsTy}, {}};
}
// And evalutes to a boolean if the LHS is falsey, and the RHS type if LHS is truthy.
SimplifyResult filteredLhs = simplifyIntersection(ctx->builtins, ctx->arena, lhsTy, ctx->builtins->falsyType);
SimplifyResult overallResult = simplifyUnion(ctx->builtins, ctx->arena, rhsTy, filteredLhs.result);
std::vector<TypeId> blockedTypes(filteredLhs.blockedTypes.begin(), filteredLhs.blockedTypes.end());
blockedTypes.insert(blockedTypes.end(), overallResult.blockedTypes.begin(), overallResult.blockedTypes.end());
return {overallResult.result, false, std::move(blockedTypes), {}};
}
TypeFamilyReductionResult<TypeId> orFamilyFn(std::vector<TypeId> typeParams, std::vector<TypePackId> packParams, NotNull<TypeFamilyContext> ctx)
{
if (typeParams.size() != 2 || !packParams.empty())
{
ctx->ice->ice("or type family: encountered a type family instance without the required argument structure");
LUAU_ASSERT(false);
}
TypeId lhsTy = follow(typeParams.at(0));
TypeId rhsTy = follow(typeParams.at(1));
if (isPending(lhsTy, ctx->solver))
{
return {std::nullopt, false, {lhsTy}, {}};
}
else if (isPending(rhsTy, ctx->solver))
{
return {std::nullopt, false, {rhsTy}, {}};
}
// Or evalutes to the LHS type if the LHS is truthy, and the RHS type if LHS is falsy.
SimplifyResult filteredLhs = simplifyIntersection(ctx->builtins, ctx->arena, lhsTy, ctx->builtins->truthyType);
SimplifyResult overallResult = simplifyUnion(ctx->builtins, ctx->arena, rhsTy, filteredLhs.result);
std::vector<TypeId> blockedTypes(filteredLhs.blockedTypes.begin(), filteredLhs.blockedTypes.end());
blockedTypes.insert(blockedTypes.end(), overallResult.blockedTypes.begin(), overallResult.blockedTypes.end());
return {overallResult.result, false, std::move(blockedTypes), {}};
}
BuiltinTypeFamilies::BuiltinTypeFamilies() BuiltinTypeFamilies::BuiltinTypeFamilies()
: addFamily{"Add", addFamilyFn} : addFamily{"Add", addFamilyFn}
, subFamily{"Sub", subFamilyFn}
, mulFamily{"Mul", mulFamilyFn}
, divFamily{"Div", divFamilyFn}
, idivFamily{"FloorDiv", idivFamilyFn}
, powFamily{"Exp", powFamilyFn}
, modFamily{"Mod", modFamilyFn}
, andFamily{"And", andFamilyFn}
, orFamily{"Or", orFamilyFn}
{ {
} }
void BuiltinTypeFamilies::addToScope(NotNull<TypeArena> arena, NotNull<Scope> scope) const
{
// make a type function for a two-argument type family
auto mkBinaryTypeFamily = [&](const TypeFamily* family) {
TypeId t = arena->addType(GenericType{"T"});
TypeId u = arena->addType(GenericType{"U"});
GenericTypeDefinition genericT{t};
GenericTypeDefinition genericU{u};
return TypeFun{{genericT, genericU}, arena->addType(TypeFamilyInstanceType{NotNull{family}, {t, u}, {}})};
};
scope->exportedTypeBindings[addFamily.name] = mkBinaryTypeFamily(&addFamily);
scope->exportedTypeBindings[subFamily.name] = mkBinaryTypeFamily(&subFamily);
scope->exportedTypeBindings[mulFamily.name] = mkBinaryTypeFamily(&mulFamily);
scope->exportedTypeBindings[divFamily.name] = mkBinaryTypeFamily(&divFamily);
scope->exportedTypeBindings[idivFamily.name] = mkBinaryTypeFamily(&idivFamily);
scope->exportedTypeBindings[powFamily.name] = mkBinaryTypeFamily(&powFamily);
scope->exportedTypeBindings[modFamily.name] = mkBinaryTypeFamily(&modFamily);
}
} // namespace Luau } // namespace Luau

View file

@ -10,7 +10,7 @@
#ifndef NOMINMAX #ifndef NOMINMAX
#define NOMINMAX #define NOMINMAX
#endif #endif
#include <Windows.h> #include <windows.h>
const size_t kPageSize = 4096; const size_t kPageSize = 4096;
#else #else

View file

@ -454,17 +454,21 @@ void Unifier::tryUnify_(TypeId subTy, TypeId superTy, bool isFunctionCall, bool
if (log.get<TypeFamilyInstanceType>(superTy)) if (log.get<TypeFamilyInstanceType>(superTy))
{ {
// We do not report errors from reducing here. This is because we will // FIXME: we should be be ICEing here because the old unifier is legacy and should not interact with type families at all.
// "double-report" errors in some cases, like when trying to unify // Unfortunately, there are, at the time of writing, still uses of the old unifier under local type inference.
// identical type family instantiations like Add<false, false> with TypeCheckLimits limits;
// Add<false, false>. reduceFamilies(
reduceFamilies(superTy, location, NotNull(types), builtinTypes, scope, normalizer, &log); superTy, location, TypeFamilyContext{NotNull(types), builtinTypes, scope, normalizer, NotNull{sharedState.iceHandler}, NotNull{&limits}});
superTy = log.follow(superTy); superTy = log.follow(superTy);
} }
if (log.get<TypeFamilyInstanceType>(subTy)) if (log.get<TypeFamilyInstanceType>(subTy))
{ {
reduceFamilies(subTy, location, NotNull(types), builtinTypes, scope, normalizer, &log); // FIXME: we should be be ICEing here because the old unifier is legacy and should not interact with type families at all.
// Unfortunately, there are, at the time of writing, still uses of the old unifier under local type inference.
TypeCheckLimits limits;
reduceFamilies(
subTy, location, TypeFamilyContext{NotNull(types), builtinTypes, scope, normalizer, NotNull{sharedState.iceHandler}, NotNull{&limits}});
subTy = log.follow(subTy); subTy = log.follow(subTy);
} }

View file

@ -154,6 +154,9 @@ bool Unifier2::unify(TypeId subTy, const FunctionType* superFn) {
if (shouldInstantiate) if (shouldInstantiate)
{ {
std::optional<TypeId> instantiated = instantiate(builtinTypes, arena, NotNull{&limits}, scope, subTy); std::optional<TypeId> instantiated = instantiate(builtinTypes, arena, NotNull{&limits}, scope, subTy);
if (!instantiated)
return false;
subFn = get<FunctionType>(*instantiated); subFn = get<FunctionType>(*instantiated);
LUAU_ASSERT(subFn); // instantiation should not make a function type _not_ a function type. LUAU_ASSERT(subFn); // instantiation should not make a function type _not_ a function type.

View file

@ -795,6 +795,11 @@ public:
const AstArray<AstGenericTypePack>& genericPacks, const AstTypeList& params, const AstArray<AstArgumentName>& paramNames, const AstArray<AstGenericTypePack>& genericPacks, const AstTypeList& params, const AstArray<AstArgumentName>& paramNames,
const AstTypeList& retTypes); const AstTypeList& retTypes);
AstStatDeclareFunction(const Location& location, const AstName& name, const AstArray<AstGenericType>& generics,
const AstArray<AstGenericTypePack>& genericPacks, const AstTypeList& params, const AstArray<AstArgumentName>& paramNames,
const AstTypeList& retTypes, bool checkedFunction);
void visit(AstVisitor* visitor) override; void visit(AstVisitor* visitor) override;
AstName name; AstName name;
@ -803,6 +808,7 @@ public:
AstTypeList params; AstTypeList params;
AstArray<AstArgumentName> paramNames; AstArray<AstArgumentName> paramNames;
AstTypeList retTypes; AstTypeList retTypes;
bool checkedFunction;
}; };
struct AstDeclaredClassProp struct AstDeclaredClassProp
@ -903,6 +909,9 @@ public:
AstTypeFunction(const Location& location, const AstArray<AstGenericType>& generics, const AstArray<AstGenericTypePack>& genericPacks, AstTypeFunction(const Location& location, const AstArray<AstGenericType>& generics, const AstArray<AstGenericTypePack>& genericPacks,
const AstTypeList& argTypes, const AstArray<std::optional<AstArgumentName>>& argNames, const AstTypeList& returnTypes); const AstTypeList& argTypes, const AstArray<std::optional<AstArgumentName>>& argNames, const AstTypeList& returnTypes);
AstTypeFunction(const Location& location, const AstArray<AstGenericType>& generics, const AstArray<AstGenericTypePack>& genericPacks,
const AstTypeList& argTypes, const AstArray<std::optional<AstArgumentName>>& argNames, const AstTypeList& returnTypes, bool checkedFunction);
void visit(AstVisitor* visitor) override; void visit(AstVisitor* visitor) override;
AstArray<AstGenericType> generics; AstArray<AstGenericType> generics;
@ -910,6 +919,7 @@ public:
AstTypeList argTypes; AstTypeList argTypes;
AstArray<std::optional<AstArgumentName>> argNames; AstArray<std::optional<AstArgumentName>> argNames;
AstTypeList returnTypes; AstTypeList returnTypes;
bool checkedFunction;
}; };
class AstTypeTypeof : public AstType class AstTypeTypeof : public AstType

View file

@ -91,7 +91,6 @@ struct Lexeme
BrokenComment, BrokenComment,
BrokenUnicode, BrokenUnicode,
BrokenInterpDoubleBrace, BrokenInterpDoubleBrace,
Error, Error,
Reserved_BEGIN, Reserved_BEGIN,
@ -116,6 +115,7 @@ struct Lexeme
ReservedTrue, ReservedTrue,
ReservedUntil, ReservedUntil,
ReservedWhile, ReservedWhile,
ReservedChecked,
Reserved_END Reserved_END
}; };

View file

@ -176,15 +176,16 @@ private:
AstTableIndexer* parseTableIndexer(); AstTableIndexer* parseTableIndexer();
AstTypeOrPack parseFunctionType(bool allowPack); AstTypeOrPack parseFunctionType(bool allowPack, bool isCheckedFunction = false);
AstType* parseFunctionTypeTail(const Lexeme& begin, AstArray<AstGenericType> generics, AstArray<AstGenericTypePack> genericPacks, AstType* parseFunctionTypeTail(const Lexeme& begin, AstArray<AstGenericType> generics, AstArray<AstGenericTypePack> genericPacks,
AstArray<AstType*> params, AstArray<std::optional<AstArgumentName>> paramNames, AstTypePack* varargAnnotation); AstArray<AstType*> params, AstArray<std::optional<AstArgumentName>> paramNames, AstTypePack* varargAnnotation,
bool isCheckedFunction = false);
AstType* parseTableType(); AstType* parseTableType(bool inDeclarationContext = false);
AstTypeOrPack parseSimpleType(bool allowPack); AstTypeOrPack parseSimpleType(bool allowPack, bool inDeclarationContext = false);
AstTypeOrPack parseTypeOrPack(); AstTypeOrPack parseTypeOrPack();
AstType* parseType(); AstType* parseType(bool inDeclarationContext = false);
AstTypePack* parseTypePack(); AstTypePack* parseTypePack();
AstTypePack* parseVariadicArgumentTypePack(); AstTypePack* parseVariadicArgumentTypePack();

View file

@ -709,6 +709,21 @@ AstStatDeclareFunction::AstStatDeclareFunction(const Location& location, const A
, params(params) , params(params)
, paramNames(paramNames) , paramNames(paramNames)
, retTypes(retTypes) , retTypes(retTypes)
, checkedFunction(false)
{
}
AstStatDeclareFunction::AstStatDeclareFunction(const Location& location, const AstName& name, const AstArray<AstGenericType>& generics,
const AstArray<AstGenericTypePack>& genericPacks, const AstTypeList& params, const AstArray<AstArgumentName>& paramNames,
const AstTypeList& retTypes, bool checkedFunction)
: AstStat(ClassIndex(), location)
, name(name)
, generics(generics)
, genericPacks(genericPacks)
, params(params)
, paramNames(paramNames)
, retTypes(retTypes)
, checkedFunction(checkedFunction)
{ {
} }
@ -817,6 +832,20 @@ AstTypeFunction::AstTypeFunction(const Location& location, const AstArray<AstGen
, argTypes(argTypes) , argTypes(argTypes)
, argNames(argNames) , argNames(argNames)
, returnTypes(returnTypes) , returnTypes(returnTypes)
, checkedFunction(false)
{
LUAU_ASSERT(argNames.size == 0 || argNames.size == argTypes.types.size);
}
AstTypeFunction::AstTypeFunction(const Location& location, const AstArray<AstGenericType>& generics, const AstArray<AstGenericTypePack>& genericPacks,
const AstTypeList& argTypes, const AstArray<std::optional<AstArgumentName>>& argNames, const AstTypeList& returnTypes, bool checkedFunction)
: AstType(ClassIndex(), location)
, generics(generics)
, genericPacks(genericPacks)
, argTypes(argTypes)
, argNames(argNames)
, returnTypes(returnTypes)
, checkedFunction(checkedFunction)
{ {
LUAU_ASSERT(argNames.size == 0 || argNames.size == argTypes.types.size); LUAU_ASSERT(argNames.size == 0 || argNames.size == argTypes.types.size);
} }

View file

@ -1,6 +1,7 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details // This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#include "Luau/Lexer.h" #include "Luau/Lexer.h"
#include "Luau/Common.h"
#include "Luau/Confusables.h" #include "Luau/Confusables.h"
#include "Luau/StringUtils.h" #include "Luau/StringUtils.h"
@ -8,6 +9,7 @@
LUAU_FASTFLAGVARIABLE(LuauFloorDivision, false) LUAU_FASTFLAGVARIABLE(LuauFloorDivision, false)
LUAU_FASTFLAGVARIABLE(LuauLexerLookaheadRemembersBraceType, false) LUAU_FASTFLAGVARIABLE(LuauLexerLookaheadRemembersBraceType, false)
LUAU_FASTFLAGVARIABLE(LuauCheckedFunctionSyntax, false)
namespace Luau namespace Luau
{ {
@ -106,7 +108,7 @@ Lexeme::Lexeme(const Location& location, Type type, const char* name)
} }
static const char* kReserved[] = {"and", "break", "do", "else", "elseif", "end", "false", "for", "function", "if", "in", "local", "nil", "not", "or", static const char* kReserved[] = {"and", "break", "do", "else", "elseif", "end", "false", "for", "function", "if", "in", "local", "nil", "not", "or",
"repeat", "return", "then", "true", "until", "while"}; "repeat", "return", "then", "true", "until", "while", "@checked"};
std::string Lexeme::toString() const std::string Lexeme::toString() const
{ {
@ -709,7 +711,7 @@ Lexeme Lexer::readNumber(const Position& start, unsigned int startOffset)
std::pair<AstName, Lexeme::Type> Lexer::readName() std::pair<AstName, Lexeme::Type> Lexer::readName()
{ {
LUAU_ASSERT(isAlpha(peekch()) || peekch() == '_'); LUAU_ASSERT(isAlpha(peekch()) || peekch() == '_' || peekch() == '@');
unsigned int startOffset = offset; unsigned int startOffset = offset;
@ -1007,7 +1009,20 @@ Lexeme Lexer::readNext()
return Lexeme(Location(start, 1), ch); return Lexeme(Location(start, 1), ch);
} }
case '@':
{
if (FFlag::LuauCheckedFunctionSyntax)
{
// We're trying to lex the token @checked
LUAU_ASSERT(peekch() == '@');
std::pair<AstName, Lexeme::Type> maybeChecked = readName();
if (maybeChecked.second != Lexeme::ReservedChecked)
return Lexeme(Location(start, position()), Lexeme::Error);
return Lexeme(Location(start, position()), maybeChecked.second, maybeChecked.first.value);
}
}
default: default:
if (isDigit(peekch())) if (isDigit(peekch()))
{ {

View file

@ -1,6 +1,7 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details // This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#include "Luau/Parser.h" #include "Luau/Parser.h"
#include "Luau/Common.h"
#include "Luau/TimeTrace.h" #include "Luau/TimeTrace.h"
#include <algorithm> #include <algorithm>
@ -15,6 +16,7 @@ LUAU_FASTINTVARIABLE(LuauRecursionLimit, 1000)
LUAU_FASTINTVARIABLE(LuauParseErrorLimit, 100) LUAU_FASTINTVARIABLE(LuauParseErrorLimit, 100)
LUAU_FASTFLAGVARIABLE(LuauParseDeclareClassIndexer, false) LUAU_FASTFLAGVARIABLE(LuauParseDeclareClassIndexer, false)
LUAU_FASTFLAG(LuauFloorDivision) LUAU_FASTFLAG(LuauFloorDivision)
LUAU_FASTFLAG(LuauCheckedFunctionSyntax)
namespace Luau namespace Luau
{ {
@ -823,8 +825,14 @@ AstStat* Parser::parseDeclaration(const Location& start)
if (lexer.current().type == Lexeme::ReservedFunction) if (lexer.current().type == Lexeme::ReservedFunction)
{ {
nextLexeme(); nextLexeme();
Name globalName = parseName("global function name"); bool checkedFunction = false;
if (FFlag::LuauCheckedFunctionSyntax && lexer.current().type == Lexeme::ReservedChecked)
{
checkedFunction = true;
nextLexeme();
}
Name globalName = parseName("global function name");
auto [generics, genericPacks] = parseGenericTypeList(/* withDefaultValues= */ false); auto [generics, genericPacks] = parseGenericTypeList(/* withDefaultValues= */ false);
MatchLexeme matchParen = lexer.current(); MatchLexeme matchParen = lexer.current();
@ -860,8 +868,8 @@ AstStat* Parser::parseDeclaration(const Location& start)
if (vararg && !varargAnnotation) if (vararg && !varargAnnotation)
return reportStatError(Location(start, end), {}, {}, "All declaration parameters must be annotated"); return reportStatError(Location(start, end), {}, {}, "All declaration parameters must be annotated");
return allocator.alloc<AstStatDeclareFunction>( return allocator.alloc<AstStatDeclareFunction>(Location(start, end), globalName.name, generics, genericPacks,
Location(start, end), globalName.name, generics, genericPacks, AstTypeList{copy(vars), varargAnnotation}, copy(varNames), retTypes); AstTypeList{copy(vars), varargAnnotation}, copy(varNames), retTypes, checkedFunction);
} }
else if (AstName(lexer.current().name) == "class") else if (AstName(lexer.current().name) == "class")
{ {
@ -940,7 +948,7 @@ AstStat* Parser::parseDeclaration(const Location& start)
{ {
expectAndConsume(':', "global variable declaration"); expectAndConsume(':', "global variable declaration");
AstType* type = parseType(); AstType* type = parseType(/* in declaration context */ true);
return allocator.alloc<AstStatDeclareGlobal>(Location(start, type->location), globalName->name, type); return allocator.alloc<AstStatDeclareGlobal>(Location(start, type->location), globalName->name, type);
} }
else else
@ -1302,7 +1310,7 @@ AstTableIndexer* Parser::parseTableIndexer()
// TablePropOrIndexer ::= TableProp | TableIndexer // TablePropOrIndexer ::= TableProp | TableIndexer
// PropList ::= TablePropOrIndexer {fieldsep TablePropOrIndexer} [fieldsep] // PropList ::= TablePropOrIndexer {fieldsep TablePropOrIndexer} [fieldsep]
// TableType ::= `{' PropList `}' // TableType ::= `{' PropList `}'
AstType* Parser::parseTableType() AstType* Parser::parseTableType(bool inDeclarationContext)
{ {
incrementRecursionCounter("type annotation"); incrementRecursionCounter("type annotation");
@ -1370,7 +1378,7 @@ AstType* Parser::parseTableType()
expectAndConsume(':', "table field"); expectAndConsume(':', "table field");
AstType* type = parseType(); AstType* type = parseType(inDeclarationContext);
props.push_back({name->name, name->location, type}); props.push_back({name->name, name->location, type});
} }
@ -1396,7 +1404,7 @@ AstType* Parser::parseTableType()
// ReturnType ::= Type | `(' TypeList `)' // ReturnType ::= Type | `(' TypeList `)'
// FunctionType ::= [`<' varlist `>'] `(' [TypeList] `)' `->` ReturnType // FunctionType ::= [`<' varlist `>'] `(' [TypeList] `)' `->` ReturnType
AstTypeOrPack Parser::parseFunctionType(bool allowPack) AstTypeOrPack Parser::parseFunctionType(bool allowPack, bool isCheckedFunction)
{ {
incrementRecursionCounter("type annotation"); incrementRecursionCounter("type annotation");
@ -1444,11 +1452,11 @@ AstTypeOrPack Parser::parseFunctionType(bool allowPack)
AstArray<std::optional<AstArgumentName>> paramNames = copy(names); AstArray<std::optional<AstArgumentName>> paramNames = copy(names);
return {parseFunctionTypeTail(begin, generics, genericPacks, paramTypes, paramNames, varargAnnotation), {}}; return {parseFunctionTypeTail(begin, generics, genericPacks, paramTypes, paramNames, varargAnnotation, isCheckedFunction), {}};
} }
AstType* Parser::parseFunctionTypeTail(const Lexeme& begin, AstArray<AstGenericType> generics, AstArray<AstGenericTypePack> genericPacks, AstType* Parser::parseFunctionTypeTail(const Lexeme& begin, AstArray<AstGenericType> generics, AstArray<AstGenericTypePack> genericPacks,
AstArray<AstType*> params, AstArray<std::optional<AstArgumentName>> paramNames, AstTypePack* varargAnnotation) AstArray<AstType*> params, AstArray<std::optional<AstArgumentName>> paramNames, AstTypePack* varargAnnotation, bool isCheckedFunction)
{ {
incrementRecursionCounter("type annotation"); incrementRecursionCounter("type annotation");
@ -1472,7 +1480,8 @@ AstType* Parser::parseFunctionTypeTail(const Lexeme& begin, AstArray<AstGenericT
auto [endLocation, returnTypeList] = parseReturnType(); auto [endLocation, returnTypeList] = parseReturnType();
AstTypeList paramTypes = AstTypeList{params, varargAnnotation}; AstTypeList paramTypes = AstTypeList{params, varargAnnotation};
return allocator.alloc<AstTypeFunction>(Location(begin.location, endLocation), generics, genericPacks, paramTypes, paramNames, returnTypeList); return allocator.alloc<AstTypeFunction>(
Location(begin.location, endLocation), generics, genericPacks, paramTypes, paramNames, returnTypeList, isCheckedFunction);
} }
// Type ::= // Type ::=
@ -1565,14 +1574,14 @@ AstTypeOrPack Parser::parseTypeOrPack()
return {parseTypeSuffix(type, begin), {}}; return {parseTypeSuffix(type, begin), {}};
} }
AstType* Parser::parseType() AstType* Parser::parseType(bool inDeclarationContext)
{ {
unsigned int oldRecursionCount = recursionCounter; unsigned int oldRecursionCount = recursionCounter;
incrementRecursionCounter("type annotation"); incrementRecursionCounter("type annotation");
Location begin = lexer.current().location; Location begin = lexer.current().location;
AstType* type = parseSimpleType(/* allowPack= */ false).type; AstType* type = parseSimpleType(/* allowPack= */ false, /* in declaration context */ inDeclarationContext).type;
recursionCounter = oldRecursionCount; recursionCounter = oldRecursionCount;
@ -1581,7 +1590,7 @@ AstType* Parser::parseType()
// Type ::= nil | Name[`.' Name] [ `<' Type [`,' ...] `>' ] | `typeof' `(' expr `)' | `{' [PropList] `}' // Type ::= nil | Name[`.' Name] [ `<' Type [`,' ...] `>' ] | `typeof' `(' expr `)' | `{' [PropList] `}'
// | [`<' varlist `>'] `(' [TypeList] `)' `->` ReturnType // | [`<' varlist `>'] `(' [TypeList] `)' `->` ReturnType
AstTypeOrPack Parser::parseSimpleType(bool allowPack) AstTypeOrPack Parser::parseSimpleType(bool allowPack, bool inDeclarationContext)
{ {
incrementRecursionCounter("type annotation"); incrementRecursionCounter("type annotation");
@ -1673,7 +1682,13 @@ AstTypeOrPack Parser::parseSimpleType(bool allowPack)
} }
else if (lexer.current().type == '{') else if (lexer.current().type == '{')
{ {
return {parseTableType(), {}}; return {parseTableType(/* inDeclarationContext */ inDeclarationContext), {}};
}
else if (FFlag::LuauCheckedFunctionSyntax && inDeclarationContext && lexer.current().type == Lexeme::ReservedChecked)
{
LUAU_ASSERT(FFlag::LuauCheckedFunctionSyntax);
nextLexeme();
return parseFunctionType(allowPack, /* isCheckedFunction */ true);
} }
else if (lexer.current().type == '(' || lexer.current().type == '<') else if (lexer.current().type == '(' || lexer.current().type == '<')
{ {

View file

@ -15,7 +15,7 @@
#ifndef NOMINMAX #ifndef NOMINMAX
#define NOMINMAX #define NOMINMAX
#endif #endif
#include <Windows.h> #include <windows.h>
#endif #endif
#ifdef __APPLE__ #ifdef __APPLE__

View file

@ -369,10 +369,9 @@ int main(int argc, char** argv)
stats.bytecode == 0 ? 0.0 : double(stats.codegen) / double(stats.bytecode), stats.readTime, stats.parseTime, stats.compileTime, stats.bytecode == 0 ? 0.0 : double(stats.codegen) / double(stats.bytecode), stats.readTime, stats.parseTime, stats.compileTime,
stats.codegenTime); stats.codegenTime);
printf("Lowering stats:\n"); printf("Lowering: regalloc failed: %d, lowering failed %d; spills to stack: %d, spills to restore: %d, max spill slot %u\n",
printf("- spills to stack: %d, spills to restore: %d, max spill slot %u\n", stats.lowerStats.spillsToSlot, stats.lowerStats.spillsToRestore, stats.lowerStats.regAllocErrors, stats.lowerStats.loweringErrors, stats.lowerStats.spillsToSlot, stats.lowerStats.spillsToRestore,
stats.lowerStats.maxSpillSlotsUsed); stats.lowerStats.maxSpillSlotsUsed);
printf("- regalloc failed: %d, lowering failed %d\n", stats.lowerStats.regAllocErrors, stats.lowerStats.loweringErrors);
} }
return failed ? 1 : 0; return failed ? 1 : 0;

View file

@ -10,7 +10,7 @@
#ifndef NOMINMAX #ifndef NOMINMAX
#define NOMINMAX #define NOMINMAX
#endif #endif
#include <Windows.h> #include <windows.h>
#else #else
#include <dirent.h> #include <dirent.h>
#include <fcntl.h> #include <fcntl.h>

View file

@ -85,6 +85,7 @@ public:
void test(OperandX64 lhs, OperandX64 rhs); void test(OperandX64 lhs, OperandX64 rhs);
void lea(OperandX64 lhs, OperandX64 rhs); void lea(OperandX64 lhs, OperandX64 rhs);
void setcc(ConditionX64 cond, OperandX64 op); void setcc(ConditionX64 cond, OperandX64 op);
void cmov(ConditionX64 cond, RegisterX64 lhs, OperandX64 rhs);
void push(OperandX64 op); void push(OperandX64 op);
void pop(OperandX64 op); void pop(OperandX64 op);

View file

@ -17,6 +17,8 @@ enum CodeGenFlags
{ {
// Only run native codegen for modules that have been marked with --!native // Only run native codegen for modules that have been marked with --!native
CodeGen_OnlyNativeModules = 1 << 0, CodeGen_OnlyNativeModules = 1 << 0,
// Run native codegen for functions that the compiler considers not profitable
CodeGen_ColdFunctions = 1 << 1,
}; };
enum class CodeGenCompilationResult enum class CodeGenCompilationResult

View file

@ -27,6 +27,11 @@ static const char* setccTextForCondition[] = {"seto", "setno", "setc", "setnc",
"setge", "setnb", "setnbe", "setna", "setnae", "setne", "setnl", "setnle", "setng", "setnge", "setz", "setnz", "setp", "setnp"}; "setge", "setnb", "setnbe", "setna", "setnae", "setne", "setnl", "setnle", "setng", "setnge", "setz", "setnz", "setp", "setnp"};
static_assert(sizeof(setccTextForCondition) / sizeof(setccTextForCondition[0]) == size_t(ConditionX64::Count), "all conditions have to be covered"); static_assert(sizeof(setccTextForCondition) / sizeof(setccTextForCondition[0]) == size_t(ConditionX64::Count), "all conditions have to be covered");
static const char* cmovTextForCondition[] = {"cmovo", "cmovno", "cmovc", "cmovnc", "cmovb", "cmovbe", "cmova", "cmovae", "cmove", "cmovl", "cmovle",
"cmovg", "cmovge", "cmovnb", "cmovnbe", "cmovna", "cmovnae", "cmovne", "cmovnl", "cmovnle", "cmovng", "cmovnge", "cmovz", "cmovnz", "cmovp",
"cmovnp"};
static_assert(sizeof(cmovTextForCondition) / sizeof(cmovTextForCondition[0]) == size_t(ConditionX64::Count), "all conditions have to be covered");
#define OP_PLUS_REG(op, reg) ((op) + (reg & 0x7)) #define OP_PLUS_REG(op, reg) ((op) + (reg & 0x7))
#define OP_PLUS_CC(op, cc) ((op) + uint8_t(cc)) #define OP_PLUS_CC(op, cc) ((op) + uint8_t(cc))
@ -404,6 +409,20 @@ void AssemblyBuilderX64::setcc(ConditionX64 cond, OperandX64 op)
commit(); commit();
} }
void AssemblyBuilderX64::cmov(ConditionX64 cond, RegisterX64 lhs, OperandX64 rhs)
{
SizeX64 size = rhs.cat == CategoryX64::reg ? rhs.base.size : rhs.memSize;
LUAU_ASSERT(size != SizeX64::byte && size == lhs.size);
if (logText)
log(cmovTextForCondition[size_t(cond)], lhs, rhs);
placeRex(lhs, rhs);
place(0x0f);
place(0x40 | codeForCondition[size_t(cond)]);
placeRegAndModRegMem(lhs, rhs);
commit();
}
void AssemblyBuilderX64::jcc(ConditionX64 cond, Label& label) void AssemblyBuilderX64::jcc(ConditionX64 cond, Label& label)
{ {
placeJcc(jccTextForCondition[size_t(cond)], label, codeForCondition[size_t(cond)]); placeJcc(jccTextForCondition[size_t(cond)], label, codeForCondition[size_t(cond)]);

View file

@ -13,7 +13,7 @@
#ifndef NOMINMAX #ifndef NOMINMAX
#define NOMINMAX #define NOMINMAX
#endif #endif
#include <Windows.h> #include <windows.h>
const size_t kPageSize = 4096; const size_t kPageSize = 4096;
#else #else

View file

@ -14,7 +14,7 @@
#ifndef NOMINMAX #ifndef NOMINMAX
#define NOMINMAX #define NOMINMAX
#endif #endif
#include <Windows.h> #include <windows.h>
#elif defined(__linux__) || defined(__APPLE__) #elif defined(__linux__) || defined(__APPLE__)

View file

@ -259,7 +259,7 @@ CodeGenCompilationResult compile(lua_State* L, int idx, unsigned int flags, Comp
return CodeGenCompilationResult::CodeGenNotInitialized; return CodeGenCompilationResult::CodeGenNotInitialized;
std::vector<Proto*> protos; std::vector<Proto*> protos;
gatherFunctions(protos, root); gatherFunctions(protos, root, flags);
// Skip protos that have been compiled during previous invocations of CodeGen::compile // Skip protos that have been compiled during previous invocations of CodeGen::compile
protos.erase(std::remove_if(protos.begin(), protos.end(), protos.erase(std::remove_if(protos.begin(), protos.end(),

View file

@ -5,6 +5,7 @@
#include "Luau/UnwindBuilder.h" #include "Luau/UnwindBuilder.h"
#include "BitUtils.h" #include "BitUtils.h"
#include "CodeGenUtils.h"
#include "NativeState.h" #include "NativeState.h"
#include "EmitCommonA64.h" #include "EmitCommonA64.h"
@ -96,42 +97,27 @@ static void emitInterrupt(AssemblyBuilderA64& build)
build.br(x0); build.br(x0);
} }
static void emitReentry(AssemblyBuilderA64& build, ModuleHelpers& helpers) static void emitContinueCall(AssemblyBuilderA64& build, ModuleHelpers& helpers)
{ {
// x0 = closure object to reentry (equal to clvalue(L->ci->func)) // x0 = closure object to reentry (equal to clvalue(L->ci->func))
// If the fallback requested an exit, we need to do this right away // If the fallback yielded, we need to do this right away
build.cbz(x0, helpers.exitNoContinueVm); // note: it's slightly cheaper to check x0 LSB; a valid Closure pointer must be aligned to 8 bytes
LUAU_ASSERT(CALL_FALLBACK_YIELD == 1);
emitUpdateBase(build); build.tbnz(x0, 0, helpers.exitNoContinueVm);
// Need to update state of the current function before we jump away // Need to update state of the current function before we jump away
build.ldr(x1, mem(x0, offsetof(Closure, l.p))); // cl->l.p aka proto build.ldr(x1, mem(x0, offsetof(Closure, l.p))); // cl->l.p aka proto
build.ldr(x2, mem(rState, offsetof(lua_State, ci))); // L->ci build.ldr(x2, mem(x1, offsetof(Proto, exectarget)));
build.cbz(x2, helpers.exitContinueVm);
// We need to check if the new frame can be executed natively
// TODO: .flags and .savedpc load below can be fused with ldp
build.ldr(w3, mem(x2, offsetof(CallInfo, flags)));
build.tbz(x3, countrz(LUA_CALLINFO_NATIVE), helpers.exitContinueVm);
build.mov(rClosure, x0); build.mov(rClosure, x0);
LUAU_ASSERT(offsetof(Proto, code) == offsetof(Proto, k) + 8); LUAU_ASSERT(offsetof(Proto, code) == offsetof(Proto, k) + 8);
build.ldp(rConstants, rCode, mem(x1, offsetof(Proto, k))); // proto->k, proto->code build.ldp(rConstants, rCode, mem(x1, offsetof(Proto, k))); // proto->k, proto->code
// Get instruction index from instruction pointer build.br(x2);
// To get instruction index from instruction pointer, we need to divide byte offset by 4
// But we will actually need to scale instruction index by 4 back to byte offset later so it cancels out
build.ldr(x2, mem(x2, offsetof(CallInfo, savedpc))); // L->ci->savedpc
build.sub(x2, x2, rCode);
// Get new instruction location and jump to it
LUAU_ASSERT(offsetof(Proto, exectarget) == offsetof(Proto, execdata) + 8);
build.ldp(x3, x4, mem(x1, offsetof(Proto, execdata)));
build.ldr(w2, mem(x3, x2));
build.add(x4, x4, x2);
build.br(x4);
} }
void emitReturn(AssemblyBuilderA64& build, ModuleHelpers& helpers) void emitReturn(AssemblyBuilderA64& build, ModuleHelpers& helpers)
@ -326,11 +312,6 @@ void assembleHelpers(AssemblyBuilderA64& build, ModuleHelpers& helpers)
build.setLabel(helpers.exitNoContinueVm); build.setLabel(helpers.exitNoContinueVm);
emitExit(build, /* continueInVm */ false); emitExit(build, /* continueInVm */ false);
if (build.logText)
build.logAppend("; reentry\n");
build.setLabel(helpers.reentry);
emitReentry(build, helpers);
if (build.logText) if (build.logText)
build.logAppend("; interrupt\n"); build.logAppend("; interrupt\n");
build.setLabel(helpers.interrupt); build.setLabel(helpers.interrupt);
@ -340,6 +321,11 @@ void assembleHelpers(AssemblyBuilderA64& build, ModuleHelpers& helpers)
build.logAppend("; return\n"); build.logAppend("; return\n");
build.setLabel(helpers.return_); build.setLabel(helpers.return_);
emitReturn(build, helpers); emitReturn(build, helpers);
if (build.logText)
build.logAppend("; continueCall\n");
build.setLabel(helpers.continueCall);
emitContinueCall(build, helpers);
} }
} // namespace A64 } // namespace A64

View file

@ -46,7 +46,15 @@ template<typename AssemblyBuilder>
static std::string getAssemblyImpl(AssemblyBuilder& build, const TValue* func, AssemblyOptions options, LoweringStats* stats) static std::string getAssemblyImpl(AssemblyBuilder& build, const TValue* func, AssemblyOptions options, LoweringStats* stats)
{ {
std::vector<Proto*> protos; std::vector<Proto*> protos;
gatherFunctions(protos, clvalue(func)->l.p); gatherFunctions(protos, clvalue(func)->l.p, /* flags= */ 0);
protos.erase(std::remove_if(protos.begin(), protos.end(), [](Proto* p) { return p == nullptr; }), protos.end());
if (protos.empty())
{
build.finalize(); // to avoid assertion in AssemblyBuilder dtor
return std::string();
}
ModuleHelpers helpers; ModuleHelpers helpers;
assembleHelpers(build, helpers); assembleHelpers(build, helpers);
@ -58,24 +66,23 @@ static std::string getAssemblyImpl(AssemblyBuilder& build, const TValue* func, A
} }
for (Proto* p : protos) for (Proto* p : protos)
if (p) {
IrBuilder ir;
ir.buildFunctionIr(p);
if (options.includeAssembly || options.includeIr)
logFunctionHeader(build, p);
if (!lowerFunction(ir, build, helpers, p, options, stats))
{ {
IrBuilder ir;
ir.buildFunctionIr(p);
if (options.includeAssembly || options.includeIr)
logFunctionHeader(build, p);
if (!lowerFunction(ir, build, helpers, p, options, stats))
{
if (build.logText)
build.logAppend("; skipping (can't lower)\n");
}
if (build.logText) if (build.logText)
build.logAppend("\n"); build.logAppend("; skipping (can't lower)\n");
} }
if (build.logText)
build.logAppend("\n");
}
if (!build.finalize()) if (!build.finalize())
return std::string(); return std::string();

View file

@ -29,7 +29,7 @@ namespace Luau
namespace CodeGen namespace CodeGen
{ {
inline void gatherFunctions(std::vector<Proto*>& results, Proto* proto) inline void gatherFunctions(std::vector<Proto*>& results, Proto* proto, unsigned int flags)
{ {
if (results.size() <= size_t(proto->bytecodeid)) if (results.size() <= size_t(proto->bytecodeid))
results.resize(proto->bytecodeid + 1); results.resize(proto->bytecodeid + 1);
@ -38,10 +38,13 @@ inline void gatherFunctions(std::vector<Proto*>& results, Proto* proto)
if (results[proto->bytecodeid]) if (results[proto->bytecodeid])
return; return;
results[proto->bytecodeid] = proto; // Only compile cold functions if requested
if ((proto->flags & LPF_NATIVE_COLD) == 0 || (flags & CodeGen_ColdFunctions) != 0)
results[proto->bytecodeid] = proto;
// Recursively traverse child protos even if we aren't compiling this one
for (int i = 0; i < proto->sizep; i++) for (int i = 0; i < proto->sizep; i++)
gatherFunctions(results, proto->p[i]); gatherFunctions(results, proto->p[i], flags);
} }
template<typename AssemblyBuilder, typename IrLowering> template<typename AssemblyBuilder, typename IrLowering>

View file

@ -275,7 +275,7 @@ Closure* callFallback(lua_State* L, StkId ra, StkId argtop, int nresults)
// yield // yield
if (n < 0) if (n < 0)
return NULL; return (Closure*)CALL_FALLBACK_YIELD;
// ci is our callinfo, cip is our parent // ci is our callinfo, cip is our parent
CallInfo* ci = L->ci; CallInfo* ci = L->ci;
@ -299,8 +299,7 @@ Closure* callFallback(lua_State* L, StkId ra, StkId argtop, int nresults)
L->top = (nresults == LUA_MULTRET) ? res : cip->top; L->top = (nresults == LUA_MULTRET) ? res : cip->top;
// keep executing current function // keep executing current function
LUAU_ASSERT(isLua(cip)); return NULL;
return clvalue(cip->func);
} }
} }

View file

@ -17,6 +17,8 @@ void forgPrepXnextFallback(lua_State* L, TValue* ra, int pc);
Closure* callProlog(lua_State* L, TValue* ra, StkId argtop, int nresults); Closure* callProlog(lua_State* L, TValue* ra, StkId argtop, int nresults);
void callEpilogC(lua_State* L, int nresults, int n); void callEpilogC(lua_State* L, int nresults, int n);
#define CALL_FALLBACK_YIELD 1
Closure* callFallback(lua_State* L, StkId ra, StkId argtop, int nresults); Closure* callFallback(lua_State* L, StkId ra, StkId argtop, int nresults);
const Instruction* executeGETGLOBAL(lua_State* L, const Instruction* pc, StkId base, TValue* k); const Instruction* executeGETGLOBAL(lua_State* L, const Instruction* pc, StkId base, TValue* k);

View file

@ -240,11 +240,6 @@ void assembleHelpers(X64::AssemblyBuilderX64& build, ModuleHelpers& helpers)
build.setLabel(helpers.exitNoContinueVm); build.setLabel(helpers.exitNoContinueVm);
emitExit(build, /* continueInVm */ false); emitExit(build, /* continueInVm */ false);
if (build.logText)
build.logAppend("; continueCallInVm\n");
build.setLabel(helpers.continueCallInVm);
emitContinueCallInVm(build);
if (build.logText) if (build.logText)
build.logAppend("; interrupt\n"); build.logAppend("; interrupt\n");
build.setLabel(helpers.interrupt); build.setLabel(helpers.interrupt);

View file

@ -29,11 +29,8 @@ struct ModuleHelpers
Label return_; Label return_;
Label interrupt; Label interrupt;
// X64
Label continueCallInVm;
// A64 // A64
Label reentry; // x0: closure Label continueCall; // x0: closure
}; };
} // namespace CodeGen } // namespace CodeGen

View file

@ -378,17 +378,6 @@ void emitUpdatePcForExit(AssemblyBuilderX64& build)
build.mov(qword[rax + offsetof(CallInfo, savedpc)], rdx); build.mov(qword[rax + offsetof(CallInfo, savedpc)], rdx);
} }
void emitContinueCallInVm(AssemblyBuilderX64& build)
{
RegisterX64 proto = rcx; // Sync with emitInstCall
build.mov(rdx, qword[proto + offsetof(Proto, code)]);
build.mov(rax, qword[rState + offsetof(lua_State, ci)]);
build.mov(qword[rax + offsetof(CallInfo, savedpc)], rdx);
emitExit(build, /* continueInVm */ true);
}
void emitReturn(AssemblyBuilderX64& build, ModuleHelpers& helpers) void emitReturn(AssemblyBuilderX64& build, ModuleHelpers& helpers)
{ {
// input: res in rdi, number of written values in ecx // input: res in rdi, number of written values in ecx

View file

@ -43,7 +43,7 @@ constexpr RegisterX64 rNativeContext = r13; // NativeContext* context
constexpr RegisterX64 rConstants = r12; // TValue* k constexpr RegisterX64 rConstants = r12; // TValue* k
constexpr unsigned kExtraLocals = 3; // Number of 8 byte slots available for specialized local variables specified below constexpr unsigned kExtraLocals = 3; // Number of 8 byte slots available for specialized local variables specified below
constexpr unsigned kSpillSlots = 5; // Number of 8 byte slots available for register allocator to spill data into constexpr unsigned kSpillSlots = 13; // Number of 8 byte slots available for register allocator to spill data into
static_assert((kExtraLocals + kSpillSlots) * 8 % 16 == 0, "locals have to preserve 16 byte alignment"); static_assert((kExtraLocals + kSpillSlots) * 8 % 16 == 0, "locals have to preserve 16 byte alignment");
constexpr uint8_t kWindowsFirstNonVolXmmReg = 6; constexpr uint8_t kWindowsFirstNonVolXmmReg = 6;
@ -216,7 +216,6 @@ void emitInterrupt(AssemblyBuilderX64& build);
void emitFallback(IrRegAllocX64& regs, AssemblyBuilderX64& build, int offset, int pcpos); void emitFallback(IrRegAllocX64& regs, AssemblyBuilderX64& build, int offset, int pcpos);
void emitUpdatePcForExit(AssemblyBuilderX64& build); void emitUpdatePcForExit(AssemblyBuilderX64& build);
void emitContinueCallInVm(AssemblyBuilderX64& build);
void emitReturn(AssemblyBuilderX64& build, ModuleHelpers& helpers); void emitReturn(AssemblyBuilderX64& build, ModuleHelpers& helpers);

View file

@ -79,36 +79,34 @@ void emitInstCall(AssemblyBuilderX64& build, ModuleHelpers& helpers, int ra, int
// Set L->top to ci->top as most function expect (no vararg) // Set L->top to ci->top as most function expect (no vararg)
build.mov(rax, qword[ci + offsetof(CallInfo, top)]); build.mov(rax, qword[ci + offsetof(CallInfo, top)]);
build.mov(qword[rState + offsetof(lua_State, top)], rax);
// But if it is vararg, update it to 'argi' // But if it is vararg, update it to 'argi'
Label skipVararg; Label skipVararg;
build.test(byte[proto + offsetof(Proto, is_vararg)], 1); build.test(byte[proto + offsetof(Proto, is_vararg)], 1);
build.jcc(ConditionX64::Zero, skipVararg); build.jcc(ConditionX64::Zero, skipVararg);
build.mov(rax, argi);
build.mov(qword[rState + offsetof(lua_State, top)], argi);
build.setLabel(skipVararg); build.setLabel(skipVararg);
// Keep executing new function build.mov(qword[rState + offsetof(lua_State, top)], rax);
// Switch current code
// ci->savedpc = p->code; // ci->savedpc = p->code;
build.mov(rax, qword[proto + offsetof(Proto, code)]); build.mov(rax, qword[proto + offsetof(Proto, code)]);
build.mov(sCode, rax); // note: this needs to be before the next store for optimal performance
build.mov(qword[ci + offsetof(CallInfo, savedpc)], rax); build.mov(qword[ci + offsetof(CallInfo, savedpc)], rax);
// Get native function entry
build.mov(rax, qword[proto + offsetof(Proto, exectarget)]);
build.test(rax, rax);
build.jcc(ConditionX64::Zero, helpers.continueCallInVm);
// Mark call frame as native
build.mov(dword[ci + offsetof(CallInfo, flags)], LUA_CALLINFO_NATIVE);
// Switch current constants // Switch current constants
build.mov(rConstants, qword[proto + offsetof(Proto, k)]); build.mov(rConstants, qword[proto + offsetof(Proto, k)]);
// Switch current code // Get native function entry
build.mov(rdx, qword[proto + offsetof(Proto, code)]); build.mov(rax, qword[proto + offsetof(Proto, exectarget)]);
build.mov(sCode, rdx); build.test(rax, rax);
build.jcc(ConditionX64::Zero, helpers.exitContinueVm);
// Mark call frame as native
build.mov(dword[ci + offsetof(CallInfo, flags)], LUA_CALLINFO_NATIVE);
build.jmp(rax); build.jmp(rax);
} }

View file

@ -146,6 +146,11 @@ void IrBuilder::buildFunctionIr(Proto* proto)
if (instIndexToBlock[i] != kNoAssociatedBlockIndex) if (instIndexToBlock[i] != kNoAssociatedBlockIndex)
beginBlock(blockAtInst(i)); beginBlock(blockAtInst(i));
// Numeric for loops require additional processing to maintain loop stack
// Notably, this must be performed even when the block is dead so that we maintain the pairing FORNPREP-FORNLOOP
if (op == LOP_FORNPREP)
beforeInstForNPrep(*this, pc);
// We skip dead bytecode instructions when they appear after block was already terminated // We skip dead bytecode instructions when they appear after block was already terminated
if (!inTerminatedBlock) if (!inTerminatedBlock)
{ {
@ -164,6 +169,10 @@ void IrBuilder::buildFunctionIr(Proto* proto)
} }
} }
// See above for FORNPREP..FORNLOOP processing
if (op == LOP_FORNLOOP)
afterInstForNLoop(*this, pc);
i = nexti; i = nexti;
LUAU_ASSERT(i <= proto->sizecode); LUAU_ASSERT(i <= proto->sizecode);

View file

@ -1515,8 +1515,10 @@ void IrLoweringA64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next)
build.ldr(x4, mem(rNativeContext, offsetof(NativeContext, callFallback))); build.ldr(x4, mem(rNativeContext, offsetof(NativeContext, callFallback)));
build.blr(x4); build.blr(x4);
// reentry with x0=closure (NULL will trigger exit) emitUpdateBase(build);
build.b(helpers.reentry);
// reentry with x0=closure (NULL implies C function; CALL_FALLBACK_YIELD will trigger exit)
build.cbnz(x0, helpers.continueCall);
break; break;
case IrCmd::RETURN: case IrCmd::RETURN:
regs.spill(build, index); regs.spill(build, index);

View file

@ -10,7 +10,7 @@
#include <string.h> #include <string.h>
LUAU_FASTFLAGVARIABLE(DebugLuauCodegenChaosA64, false) LUAU_FASTFLAGVARIABLE(DebugCodegenChaosA64, false)
namespace Luau namespace Luau
{ {
@ -146,7 +146,7 @@ RegisterA64 IrRegAllocA64::allocReg(KindA64 kind, uint32_t index)
int reg = 31 - countlz(set.free); int reg = 31 - countlz(set.free);
if (FFlag::DebugLuauCodegenChaosA64) if (FFlag::DebugCodegenChaosA64)
reg = countrz(set.free); // allocate from low end; this causes extra conflicts for calls reg = countrz(set.free); // allocate from low end; this causes extra conflicts for calls
set.free &= ~(1u << reg); set.free &= ~(1u << reg);
@ -167,7 +167,7 @@ RegisterA64 IrRegAllocA64::allocTemp(KindA64 kind)
int reg = 31 - countlz(set.free); int reg = 31 - countlz(set.free);
if (FFlag::DebugLuauCodegenChaosA64) if (FFlag::DebugCodegenChaosA64)
reg = countrz(set.free); // allocate from low end; this causes extra conflicts for calls reg = countrz(set.free); // allocate from low end; this causes extra conflicts for calls
set.free &= ~(1u << reg); set.free &= ~(1u << reg);
@ -278,7 +278,7 @@ size_t IrRegAllocA64::spill(AssemblyBuilderA64& build, uint32_t index, std::init
uint32_t poisongpr = 0; uint32_t poisongpr = 0;
uint32_t poisonsimd = 0; uint32_t poisonsimd = 0;
if (FFlag::DebugLuauCodegenChaosA64) if (FFlag::DebugCodegenChaosA64)
{ {
poisongpr = gpr.base & ~gpr.free; poisongpr = gpr.base & ~gpr.free;
poisonsimd = simd.base & ~simd.free; poisonsimd = simd.base & ~simd.free;
@ -370,7 +370,7 @@ size_t IrRegAllocA64::spill(AssemblyBuilderA64& build, uint32_t index, std::init
LUAU_ASSERT(set.free == set.base); LUAU_ASSERT(set.free == set.base);
} }
if (FFlag::DebugLuauCodegenChaosA64) if (FFlag::DebugCodegenChaosA64)
{ {
for (int reg = 0; reg < 32; ++reg) for (int reg = 0; reg < 32; ++reg)
{ {

View file

@ -83,6 +83,9 @@ static BuiltinImplResult translateBuiltin2NumberToNumberLibm(
IrOp va = builtinLoadDouble(build, build.vmReg(arg)); IrOp va = builtinLoadDouble(build, build.vmReg(arg));
IrOp vb = builtinLoadDouble(build, args); IrOp vb = builtinLoadDouble(build, args);
if (bfid == LBF_MATH_LDEXP)
vb = build.inst(IrCmd::NUM_TO_INT, vb);
IrOp res = build.inst(IrCmd::INVOKE_LIBM, build.constUint(bfid), va, vb); IrOp res = build.inst(IrCmd::INVOKE_LIBM, build.constUint(bfid), va, vb);
build.inst(IrCmd::STORE_DOUBLE, build.vmReg(ra), res); build.inst(IrCmd::STORE_DOUBLE, build.vmReg(ra), res);
@ -93,30 +96,6 @@ static BuiltinImplResult translateBuiltin2NumberToNumberLibm(
return {BuiltinImplType::Full, 1}; return {BuiltinImplType::Full, 1};
} }
static BuiltinImplResult translateBuiltinMathLdexp(
IrBuilder& build, LuauBuiltinFunction bfid, int nparams, int ra, int arg, IrOp args, int nresults, int pcpos)
{
if (nparams < 2 || nresults > 1)
return {BuiltinImplType::None, -1};
builtinCheckDouble(build, build.vmReg(arg), pcpos);
builtinCheckDouble(build, args, pcpos);
IrOp va = builtinLoadDouble(build, build.vmReg(arg));
IrOp vb = builtinLoadDouble(build, args);
IrOp vbi = build.inst(IrCmd::NUM_TO_INT, vb);
IrOp res = build.inst(IrCmd::INVOKE_LIBM, build.constUint(bfid), va, vbi);
build.inst(IrCmd::STORE_DOUBLE, build.vmReg(ra), res);
if (ra != arg)
build.inst(IrCmd::STORE_TAG, build.vmReg(ra), build.constTag(LUA_TNUMBER));
return {BuiltinImplType::Full, 1};
}
// (number, ...) -> (number, number) // (number, ...) -> (number, number)
static BuiltinImplResult translateBuiltinNumberTo2Number( static BuiltinImplResult translateBuiltinNumberTo2Number(
IrBuilder& build, LuauBuiltinFunction bfid, int nparams, int ra, int arg, IrOp args, int nresults, int pcpos) IrBuilder& build, LuauBuiltinFunction bfid, int nparams, int ra, int arg, IrOp args, int nresults, int pcpos)
@ -152,7 +131,7 @@ static BuiltinImplResult translateBuiltinAssert(IrBuilder& build, int nparams, i
return {BuiltinImplType::UsesFallback, 0}; return {BuiltinImplType::UsesFallback, 0};
} }
static BuiltinImplResult translateBuiltinMathDeg(IrBuilder& build, int nparams, int ra, int arg, IrOp args, int nresults, int pcpos) static BuiltinImplResult translateBuiltinMathDegRad(IrBuilder& build, IrCmd cmd, int nparams, int ra, int arg, IrOp args, int nresults, int pcpos)
{ {
if (nparams < 1 || nresults > 1) if (nparams < 1 || nresults > 1)
return {BuiltinImplType::None, -1}; return {BuiltinImplType::None, -1};
@ -162,26 +141,7 @@ static BuiltinImplResult translateBuiltinMathDeg(IrBuilder& build, int nparams,
const double rpd = (3.14159265358979323846 / 180.0); const double rpd = (3.14159265358979323846 / 180.0);
IrOp varg = builtinLoadDouble(build, build.vmReg(arg)); IrOp varg = builtinLoadDouble(build, build.vmReg(arg));
IrOp value = build.inst(IrCmd::DIV_NUM, varg, build.constDouble(rpd)); IrOp value = build.inst(cmd, varg, build.constDouble(rpd));
build.inst(IrCmd::STORE_DOUBLE, build.vmReg(ra), value);
if (ra != arg)
build.inst(IrCmd::STORE_TAG, build.vmReg(ra), build.constTag(LUA_TNUMBER));
return {BuiltinImplType::Full, 1};
}
static BuiltinImplResult translateBuiltinMathRad(IrBuilder& build, int nparams, int ra, int arg, IrOp args, int nresults, int pcpos)
{
if (nparams < 1 || nresults > 1)
return {BuiltinImplType::None, -1};
builtinCheckDouble(build, build.vmReg(arg), pcpos);
const double rpd = (3.14159265358979323846 / 180.0);
IrOp varg = builtinLoadDouble(build, build.vmReg(arg));
IrOp value = build.inst(IrCmd::MUL_NUM, varg, build.constDouble(rpd));
build.inst(IrCmd::STORE_DOUBLE, build.vmReg(ra), value); build.inst(IrCmd::STORE_DOUBLE, build.vmReg(ra), value);
if (ra != arg) if (ra != arg)
@ -231,7 +191,7 @@ static BuiltinImplResult translateBuiltinMathLog(
return {BuiltinImplType::Full, 1}; return {BuiltinImplType::Full, 1};
} }
static BuiltinImplResult translateBuiltinMathMin(IrBuilder& build, int nparams, int ra, int arg, IrOp args, int nresults, int pcpos) static BuiltinImplResult translateBuiltinMathMinMax(IrBuilder& build, IrCmd cmd, int nparams, int ra, int arg, IrOp args, int nresults, int pcpos)
{ {
if (nparams < 2 || nparams > kMinMaxUnrolledParams || nresults > 1) if (nparams < 2 || nparams > kMinMaxUnrolledParams || nresults > 1)
return {BuiltinImplType::None, -1}; return {BuiltinImplType::None, -1};
@ -245,42 +205,12 @@ static BuiltinImplResult translateBuiltinMathMin(IrBuilder& build, int nparams,
IrOp varg1 = builtinLoadDouble(build, build.vmReg(arg)); IrOp varg1 = builtinLoadDouble(build, build.vmReg(arg));
IrOp varg2 = builtinLoadDouble(build, args); IrOp varg2 = builtinLoadDouble(build, args);
IrOp res = build.inst(IrCmd::MIN_NUM, varg2, varg1); // Swapped arguments are required for consistency with VM builtins IrOp res = build.inst(cmd, varg2, varg1); // Swapped arguments are required for consistency with VM builtins
for (int i = 3; i <= nparams; ++i) for (int i = 3; i <= nparams; ++i)
{ {
IrOp arg = builtinLoadDouble(build, build.vmReg(vmRegOp(args) + (i - 2))); IrOp arg = builtinLoadDouble(build, build.vmReg(vmRegOp(args) + (i - 2)));
res = build.inst(IrCmd::MIN_NUM, arg, res); res = build.inst(cmd, arg, res);
}
build.inst(IrCmd::STORE_DOUBLE, build.vmReg(ra), res);
if (ra != arg)
build.inst(IrCmd::STORE_TAG, build.vmReg(ra), build.constTag(LUA_TNUMBER));
return {BuiltinImplType::Full, 1};
}
static BuiltinImplResult translateBuiltinMathMax(IrBuilder& build, int nparams, int ra, int arg, IrOp args, int nresults, int pcpos)
{
if (nparams < 2 || nparams > kMinMaxUnrolledParams || nresults > 1)
return {BuiltinImplType::None, -1};
builtinCheckDouble(build, build.vmReg(arg), pcpos);
builtinCheckDouble(build, args, pcpos);
for (int i = 3; i <= nparams; ++i)
builtinCheckDouble(build, build.vmReg(vmRegOp(args) + (i - 2)), pcpos);
IrOp varg1 = builtinLoadDouble(build, build.vmReg(arg));
IrOp varg2 = builtinLoadDouble(build, args);
IrOp res = build.inst(IrCmd::MAX_NUM, varg2, varg1); // Swapped arguments are required for consistency with VM builtins
for (int i = 3; i <= nparams; ++i)
{
IrOp arg = builtinLoadDouble(build, build.vmReg(vmRegOp(args) + (i - 2)));
res = build.inst(IrCmd::MAX_NUM, arg, res);
} }
build.inst(IrCmd::STORE_DOUBLE, build.vmReg(ra), res); build.inst(IrCmd::STORE_DOUBLE, build.vmReg(ra), res);
@ -823,15 +753,15 @@ BuiltinImplResult translateBuiltin(IrBuilder& build, int bfid, int ra, int arg,
case LBF_ASSERT: case LBF_ASSERT:
return translateBuiltinAssert(build, nparams, ra, arg, args, nresults, pcpos); return translateBuiltinAssert(build, nparams, ra, arg, args, nresults, pcpos);
case LBF_MATH_DEG: case LBF_MATH_DEG:
return translateBuiltinMathDeg(build, nparams, ra, arg, args, nresults, pcpos); return translateBuiltinMathDegRad(build, IrCmd::DIV_NUM, nparams, ra, arg, args, nresults, pcpos);
case LBF_MATH_RAD: case LBF_MATH_RAD:
return translateBuiltinMathRad(build, nparams, ra, arg, args, nresults, pcpos); return translateBuiltinMathDegRad(build, IrCmd::MUL_NUM, nparams, ra, arg, args, nresults, pcpos);
case LBF_MATH_LOG: case LBF_MATH_LOG:
return translateBuiltinMathLog(build, LuauBuiltinFunction(bfid), nparams, ra, arg, args, nresults, pcpos); return translateBuiltinMathLog(build, LuauBuiltinFunction(bfid), nparams, ra, arg, args, nresults, pcpos);
case LBF_MATH_MIN: case LBF_MATH_MIN:
return translateBuiltinMathMin(build, nparams, ra, arg, args, nresults, pcpos); return translateBuiltinMathMinMax(build, IrCmd::MIN_NUM, nparams, ra, arg, args, nresults, pcpos);
case LBF_MATH_MAX: case LBF_MATH_MAX:
return translateBuiltinMathMax(build, nparams, ra, arg, args, nresults, pcpos); return translateBuiltinMathMinMax(build, IrCmd::MAX_NUM, nparams, ra, arg, args, nresults, pcpos);
case LBF_MATH_CLAMP: case LBF_MATH_CLAMP:
return translateBuiltinMathClamp(build, nparams, ra, arg, args, nresults, fallback, pcpos); return translateBuiltinMathClamp(build, nparams, ra, arg, args, nresults, fallback, pcpos);
case LBF_MATH_FLOOR: case LBF_MATH_FLOOR:
@ -861,9 +791,8 @@ BuiltinImplResult translateBuiltin(IrBuilder& build, int bfid, int ra, int arg,
case LBF_MATH_POW: case LBF_MATH_POW:
case LBF_MATH_FMOD: case LBF_MATH_FMOD:
case LBF_MATH_ATAN2: case LBF_MATH_ATAN2:
return translateBuiltin2NumberToNumberLibm(build, LuauBuiltinFunction(bfid), nparams, ra, arg, args, nresults, pcpos);
case LBF_MATH_LDEXP: case LBF_MATH_LDEXP:
return translateBuiltinMathLdexp(build, LuauBuiltinFunction(bfid), nparams, ra, arg, args, nresults, pcpos); return translateBuiltin2NumberToNumberLibm(build, LuauBuiltinFunction(bfid), nparams, ra, arg, args, nresults, pcpos);
case LBF_MATH_FREXP: case LBF_MATH_FREXP:
case LBF_MATH_MODF: case LBF_MATH_MODF:
return translateBuiltinNumberTo2Number(build, LuauBuiltinFunction(bfid), nparams, ra, arg, args, nresults, pcpos); return translateBuiltinNumberTo2Number(build, LuauBuiltinFunction(bfid), nparams, ra, arg, args, nresults, pcpos);

View file

@ -12,8 +12,9 @@
#include "lstate.h" #include "lstate.h"
#include "ltm.h" #include "ltm.h"
LUAU_FASTFLAGVARIABLE(LuauImproveForN, false) LUAU_FASTFLAGVARIABLE(LuauImproveForN2, false)
LUAU_FASTFLAG(LuauReduceStackSpills) LUAU_FASTFLAG(LuauReduceStackSpills)
LUAU_FASTFLAGVARIABLE(LuauInlineArrConstOffset, false)
namespace Luau namespace Luau
{ {
@ -631,6 +632,26 @@ static IrOp getLoopStepK(IrBuilder& build, int ra)
return build.undef(); return build.undef();
} }
void beforeInstForNPrep(IrBuilder& build, const Instruction* pc)
{
if (FFlag::LuauImproveForN2)
{
int ra = LUAU_INSN_A(*pc);
IrOp stepK = getLoopStepK(build, ra);
build.loopStepStack.push_back(stepK);
}
}
void afterInstForNLoop(IrBuilder& build, const Instruction* pc)
{
if (FFlag::LuauImproveForN2)
{
LUAU_ASSERT(!build.loopStepStack.empty());
build.loopStepStack.pop_back();
}
}
void translateInstForNPrep(IrBuilder& build, const Instruction* pc, int pcpos) void translateInstForNPrep(IrBuilder& build, const Instruction* pc, int pcpos)
{ {
int ra = LUAU_INSN_A(*pc); int ra = LUAU_INSN_A(*pc);
@ -638,10 +659,10 @@ void translateInstForNPrep(IrBuilder& build, const Instruction* pc, int pcpos)
IrOp loopStart = build.blockAtInst(pcpos + getOpLength(LuauOpcode(LUAU_INSN_OP(*pc)))); IrOp loopStart = build.blockAtInst(pcpos + getOpLength(LuauOpcode(LUAU_INSN_OP(*pc))));
IrOp loopExit = build.blockAtInst(getJumpTarget(*pc, pcpos)); IrOp loopExit = build.blockAtInst(getJumpTarget(*pc, pcpos));
if (FFlag::LuauImproveForN) if (FFlag::LuauImproveForN2)
{ {
IrOp stepK = getLoopStepK(build, ra); LUAU_ASSERT(!build.loopStepStack.empty());
build.loopStepStack.push_back(stepK); IrOp stepK = build.loopStepStack.back();
// When loop parameters are not numbers, VM tries to perform type coercion from string and raises an exception if that fails // 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 // Performing that fallback in native code increases code size and complicates CFG, obscuring the values when they are constant
@ -733,7 +754,7 @@ void translateInstForNPrep(IrBuilder& build, const Instruction* pc, int pcpos)
// VM places interrupt in FORNLOOP, but that creates a likely spill point for short loops that use loop index as INTERRUPT always spills // 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. // 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. // 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) if (FFlag::LuauImproveForN2)
build.interruptRequested = true; build.interruptRequested = true;
} }
@ -744,11 +765,10 @@ void translateInstForNLoop(IrBuilder& build, const Instruction* pc, int pcpos)
IrOp loopRepeat = build.blockAtInst(getJumpTarget(*pc, pcpos)); IrOp loopRepeat = build.blockAtInst(getJumpTarget(*pc, pcpos));
IrOp loopExit = build.blockAtInst(pcpos + getOpLength(LuauOpcode(LUAU_INSN_OP(*pc)))); IrOp loopExit = build.blockAtInst(pcpos + getOpLength(LuauOpcode(LUAU_INSN_OP(*pc))));
if (FFlag::LuauImproveForN) if (FFlag::LuauImproveForN2)
{ {
LUAU_ASSERT(!build.loopStepStack.empty()); LUAU_ASSERT(!build.loopStepStack.empty());
IrOp stepK = build.loopStepStack.back(); IrOp stepK = build.loopStepStack.back();
build.loopStepStack.pop_back();
IrOp zero = build.constDouble(0.0); IrOp zero = build.constDouble(0.0);
IrOp limit = build.inst(IrCmd::LOAD_DOUBLE, build.vmReg(ra + 0)); IrOp limit = build.inst(IrCmd::LOAD_DOUBLE, build.vmReg(ra + 0));
@ -950,10 +970,20 @@ void translateInstGetTableN(IrBuilder& build, const Instruction* pc, int pcpos)
build.inst(IrCmd::CHECK_ARRAY_SIZE, vb, build.constInt(c), fallback); build.inst(IrCmd::CHECK_ARRAY_SIZE, vb, build.constInt(c), fallback);
build.inst(IrCmd::CHECK_NO_METATABLE, vb, fallback); build.inst(IrCmd::CHECK_NO_METATABLE, vb, fallback);
IrOp arrEl = build.inst(IrCmd::GET_ARR_ADDR, vb, build.constInt(c)); if (FFlag::LuauInlineArrConstOffset)
{
IrOp arrEl = build.inst(IrCmd::GET_ARR_ADDR, vb, build.constInt(0));
IrOp arrElTval = build.inst(IrCmd::LOAD_TVALUE, arrEl); IrOp arrElTval = build.inst(IrCmd::LOAD_TVALUE, arrEl, build.constInt(c * sizeof(TValue)));
build.inst(IrCmd::STORE_TVALUE, build.vmReg(ra), arrElTval); build.inst(IrCmd::STORE_TVALUE, build.vmReg(ra), arrElTval);
}
else
{
IrOp arrEl = build.inst(IrCmd::GET_ARR_ADDR, vb, build.constInt(c));
IrOp arrElTval = build.inst(IrCmd::LOAD_TVALUE, arrEl);
build.inst(IrCmd::STORE_TVALUE, build.vmReg(ra), arrElTval);
}
IrOp next = build.blockAtInst(pcpos + 1); IrOp next = build.blockAtInst(pcpos + 1);
FallbackStreamScope scope(build, fallback, next); FallbackStreamScope scope(build, fallback, next);
@ -980,10 +1010,20 @@ void translateInstSetTableN(IrBuilder& build, const Instruction* pc, int pcpos)
build.inst(IrCmd::CHECK_NO_METATABLE, vb, fallback); build.inst(IrCmd::CHECK_NO_METATABLE, vb, fallback);
build.inst(IrCmd::CHECK_READONLY, vb, fallback); build.inst(IrCmd::CHECK_READONLY, vb, fallback);
IrOp arrEl = build.inst(IrCmd::GET_ARR_ADDR, vb, build.constInt(c)); if (FFlag::LuauInlineArrConstOffset)
{
IrOp arrEl = build.inst(IrCmd::GET_ARR_ADDR, vb, build.constInt(0));
IrOp tva = build.inst(IrCmd::LOAD_TVALUE, build.vmReg(ra)); IrOp tva = build.inst(IrCmd::LOAD_TVALUE, build.vmReg(ra));
build.inst(IrCmd::STORE_TVALUE, arrEl, tva); build.inst(IrCmd::STORE_TVALUE, arrEl, tva, build.constInt(c * sizeof(TValue)));
}
else
{
IrOp arrEl = build.inst(IrCmd::GET_ARR_ADDR, vb, build.constInt(c));
IrOp tva = build.inst(IrCmd::LOAD_TVALUE, build.vmReg(ra));
build.inst(IrCmd::STORE_TVALUE, arrEl, tva);
}
build.inst(IrCmd::BARRIER_TABLE_FORWARD, vb, build.vmReg(ra), build.undef()); build.inst(IrCmd::BARRIER_TABLE_FORWARD, vb, build.vmReg(ra), build.undef());

View file

@ -65,5 +65,8 @@ void translateInstAndX(IrBuilder& build, const Instruction* pc, int pcpos, IrOp
void translateInstOrX(IrBuilder& build, const Instruction* pc, int pcpos, IrOp c); void translateInstOrX(IrBuilder& build, const Instruction* pc, int pcpos, IrOp c);
void translateInstNewClosure(IrBuilder& build, const Instruction* pc, int pcpos); void translateInstNewClosure(IrBuilder& build, const Instruction* pc, int pcpos);
void beforeInstForNPrep(IrBuilder& build, const Instruction* pc);
void afterInstForNLoop(IrBuilder& build, const Instruction* pc);
} // namespace CodeGen } // namespace CodeGen
} // namespace Luau } // namespace Luau

View file

@ -427,7 +427,7 @@ enum LuauBytecodeTag
// Bytecode version; runtime supports [MIN, MAX], compiler emits TARGET by default but may emit a higher version when flags are enabled // Bytecode version; runtime supports [MIN, MAX], compiler emits TARGET by default but may emit a higher version when flags are enabled
LBC_VERSION_MIN = 3, LBC_VERSION_MIN = 3,
LBC_VERSION_MAX = 4, LBC_VERSION_MAX = 4,
LBC_VERSION_TARGET = 3, LBC_VERSION_TARGET = 4,
// Type encoding version // Type encoding version
LBC_TYPE_VERSION = 1, LBC_TYPE_VERSION = 1,
// Types of constant table entries // Types of constant table entries
@ -575,4 +575,6 @@ enum LuauProtoFlag
{ {
// used to tag main proto for modules with --!native // used to tag main proto for modules with --!native
LPF_NATIVE_MODULE = 1 << 0, LPF_NATIVE_MODULE = 1 << 0,
// used to tag individual protos as not profitable to compile natively
LPF_NATIVE_COLD = 1 << 1,
}; };

View file

@ -7,8 +7,6 @@
#include <algorithm> #include <algorithm>
#include <string.h> #include <string.h>
LUAU_FASTFLAGVARIABLE(BytecodeVersion4, false)
LUAU_FASTFLAG(LuauFloorDivision) LUAU_FASTFLAG(LuauFloorDivision)
namespace Luau namespace Luau
@ -586,12 +584,9 @@ void BytecodeBuilder::finalize()
bytecode = char(version); bytecode = char(version);
if (FFlag::BytecodeVersion4) uint8_t typesversion = getTypeEncodingVersion();
{ LUAU_ASSERT(typesversion == 1);
uint8_t typesversion = getTypeEncodingVersion(); writeByte(bytecode, typesversion);
LUAU_ASSERT(typesversion == 1);
writeByte(bytecode, typesversion);
}
writeStringTable(bytecode); writeStringTable(bytecode);
@ -615,13 +610,10 @@ void BytecodeBuilder::writeFunction(std::string& ss, uint32_t id, uint8_t flags)
writeByte(ss, func.numupvalues); writeByte(ss, func.numupvalues);
writeByte(ss, func.isvararg); writeByte(ss, func.isvararg);
if (FFlag::BytecodeVersion4) writeByte(ss, flags);
{
writeByte(ss, flags);
writeVarInt(ss, uint32_t(func.typeinfo.size())); writeVarInt(ss, uint32_t(func.typeinfo.size()));
ss.append(func.typeinfo); ss.append(func.typeinfo);
}
// instructions // instructions
writeVarInt(ss, uint32_t(insns.size())); writeVarInt(ss, uint32_t(insns.size()));
@ -1074,10 +1066,6 @@ std::string BytecodeBuilder::getError(const std::string& message)
uint8_t BytecodeBuilder::getVersion() uint8_t BytecodeBuilder::getVersion()
{ {
// This function usually returns LBC_VERSION_TARGET but may sometimes return a higher number (within LBC_VERSION_MIN/MAX) under fast flags // This function usually returns LBC_VERSION_TARGET but may sometimes return a higher number (within LBC_VERSION_MIN/MAX) under fast flags
if (FFlag::BytecodeVersion4)
return 4;
return LBC_VERSION_TARGET; return LBC_VERSION_TARGET;
} }

View file

@ -26,7 +26,13 @@ LUAU_FASTINTVARIABLE(LuauCompileInlineThreshold, 25)
LUAU_FASTINTVARIABLE(LuauCompileInlineThresholdMaxBoost, 300) LUAU_FASTINTVARIABLE(LuauCompileInlineThresholdMaxBoost, 300)
LUAU_FASTINTVARIABLE(LuauCompileInlineDepth, 5) LUAU_FASTINTVARIABLE(LuauCompileInlineDepth, 5)
LUAU_FASTFLAGVARIABLE(LuauCompileFenvNoBuiltinFold, false)
LUAU_FASTFLAGVARIABLE(LuauCompileTopCold, false)
LUAU_FASTFLAG(LuauFloorDivision) LUAU_FASTFLAG(LuauFloorDivision)
LUAU_FASTFLAGVARIABLE(LuauCompileFixContinueValidation, false)
LUAU_FASTFLAGVARIABLE(LuauCompileContinueCloseUpvals, false)
namespace Luau namespace Luau
{ {
@ -259,6 +265,10 @@ struct Compiler
if (bytecode.getInstructionCount() > kMaxInstructionCount) if (bytecode.getInstructionCount() > kMaxInstructionCount)
CompileError::raise(func->location, "Exceeded function instruction limit; split the function into parts to compile"); CompileError::raise(func->location, "Exceeded function instruction limit; split the function into parts to compile");
// since top-level code only executes once, it can be marked as cold if it has no loops (top-level code with loops might be profitable to compile natively)
if (FFlag::LuauCompileTopCold && func->functionDepth == 0 && !hasLoops)
protoflags |= LPF_NATIVE_COLD;
bytecode.endFunction(uint8_t(stackSize), uint8_t(upvals.size()), protoflags); bytecode.endFunction(uint8_t(stackSize), uint8_t(upvals.size()), protoflags);
Function& f = functions[func]; Function& f = functions[func];
@ -283,6 +293,7 @@ struct Compiler
upvals.clear(); // note: instead of std::move above, we copy & clear to preserve capacity for future pushes upvals.clear(); // note: instead of std::move above, we copy & clear to preserve capacity for future pushes
stackSize = 0; stackSize = 0;
hasLoops = false;
return fid; return fid;
} }
@ -646,11 +657,25 @@ struct Compiler
// apply all evaluated arguments to the compiler state // apply all evaluated arguments to the compiler state
// note: locals use current startpc for debug info, although some of them have been computed earlier; this is similar to compileStatLocal // note: locals use current startpc for debug info, although some of them have been computed earlier; this is similar to compileStatLocal
for (InlineArg& arg : args) for (InlineArg& arg : args)
{
if (arg.value.type == Constant::Type_Unknown) if (arg.value.type == Constant::Type_Unknown)
{
pushLocal(arg.local, arg.reg); pushLocal(arg.local, arg.reg);
}
else else
{
locstants[arg.local] = arg.value; locstants[arg.local] = arg.value;
if (FFlag::LuauCompileFixContinueValidation)
{
// Mark that optimization skipped allocation of this local
Local& l = locals[arg.local];
LUAU_ASSERT(!l.skipped);
l.skipped = true;
}
}
}
// the inline frame will be used to compile return statements as well as to reject recursive inlining attempts // the inline frame will be used to compile return statements as well as to reject recursive inlining attempts
inlineFrames.push_back({func, oldLocals, target, targetCount}); inlineFrames.push_back({func, oldLocals, target, targetCount});
@ -693,8 +718,27 @@ struct Compiler
// clean up constant state for future inlining attempts // clean up constant state for future inlining attempts
for (size_t i = 0; i < func->args.size; ++i) for (size_t i = 0; i < func->args.size; ++i)
if (Constant* var = locstants.find(func->args.data[i])) {
var->type = Constant::Type_Unknown; AstLocal* local = func->args.data[i];
if (FFlag::LuauCompileFixContinueValidation)
{
if (Constant* var = locstants.find(local); var && var->type != Constant::Type_Unknown)
{
var->type = Constant::Type_Unknown;
// Restore local allocation skip flag as well
Local& l = locals[local];
LUAU_ASSERT(l.skipped);
l.skipped = false;
}
}
else
{
if (Constant* var = locstants.find(local))
var->type = Constant::Type_Unknown;
}
}
foldConstants(constants, variables, locstants, builtinsFold, builtinsFoldMathK, func->body); foldConstants(constants, variables, locstants, builtinsFold, builtinsFoldMathK, func->body);
} }
@ -2469,7 +2513,7 @@ struct Compiler
AstStat* continueStatement = extractStatContinue(stat->thenbody); AstStat* continueStatement = extractStatContinue(stat->thenbody);
// Optimization: body is a "continue" statement with no "else" => we can directly continue in "then" case // Optimization: body is a "continue" statement with no "else" => we can directly continue in "then" case
if (!stat->elsebody && continueStatement != nullptr && !areLocalsCaptured(loops.back().localOffset)) if (!stat->elsebody && continueStatement != nullptr && !areLocalsCaptured(loops.back().localOffsetContinue))
{ {
if (loops.back().untilCondition) if (loops.back().untilCondition)
validateContinueUntil(continueStatement, loops.back().untilCondition); validateContinueUntil(continueStatement, loops.back().untilCondition);
@ -2533,7 +2577,8 @@ struct Compiler
size_t oldJumps = loopJumps.size(); size_t oldJumps = loopJumps.size();
size_t oldLocals = localStack.size(); size_t oldLocals = localStack.size();
loops.push_back({oldLocals, nullptr}); loops.push_back({oldLocals, oldLocals, nullptr});
hasLoops = true;
size_t loopLabel = bytecode.emitLabel(); size_t loopLabel = bytecode.emitLabel();
@ -2568,7 +2613,8 @@ struct Compiler
size_t oldJumps = loopJumps.size(); size_t oldJumps = loopJumps.size();
size_t oldLocals = localStack.size(); size_t oldLocals = localStack.size();
loops.push_back({oldLocals, stat->condition}); loops.push_back({oldLocals, oldLocals, stat->condition});
hasLoops = true;
size_t loopLabel = bytecode.emitLabel(); size_t loopLabel = bytecode.emitLabel();
@ -2579,8 +2625,17 @@ struct Compiler
RegScope rs(this); RegScope rs(this);
for (size_t i = 0; i < body->body.size; ++i) for (size_t i = 0; i < body->body.size; ++i)
{
compileStat(body->body.data[i]); compileStat(body->body.data[i]);
// continue statement inside the repeat..until loop should not close upvalues defined directly in the loop body
// (but it must still close upvalues defined in more nested blocks)
// this is because the upvalues defined inside the loop body may be captured by a closure defined in the until
// expression that continue will jump to.
if (FFlag::LuauCompileContinueCloseUpvals)
loops.back().localOffsetContinue = localStack.size();
}
size_t contLabel = bytecode.emitLabel(); size_t contLabel = bytecode.emitLabel();
size_t endLabel; size_t endLabel;
@ -2707,7 +2762,19 @@ struct Compiler
{ {
// Optimization: we don't need to allocate and assign const locals, since their uses will be constant-folded // Optimization: we don't need to allocate and assign const locals, since their uses will be constant-folded
if (options.optimizationLevel >= 1 && options.debugLevel <= 1 && areLocalsRedundant(stat)) if (options.optimizationLevel >= 1 && options.debugLevel <= 1 && areLocalsRedundant(stat))
{
if (FFlag::LuauCompileFixContinueValidation)
{
// Mark that optimization skipped allocation of this local
for (AstLocal* local : stat->vars)
{
Local& l = locals[local];
l.skipped = true;
}
}
return; return;
}
// Optimization: for 1-1 local assignments, we can reuse the register *if* neither local is mutated // Optimization: for 1-1 local assignments, we can reuse the register *if* neither local is mutated
if (options.optimizationLevel >= 1 && stat->vars.size == 1 && stat->values.size == 1) if (options.optimizationLevel >= 1 && stat->vars.size == 1 && stat->values.size == 1)
@ -2796,7 +2863,7 @@ struct Compiler
size_t oldLocals = localStack.size(); size_t oldLocals = localStack.size();
size_t oldJumps = loopJumps.size(); size_t oldJumps = loopJumps.size();
loops.push_back({oldLocals, nullptr}); loops.push_back({oldLocals, oldLocals, nullptr});
for (int iv = 0; iv < tripCount; ++iv) for (int iv = 0; iv < tripCount; ++iv)
{ {
@ -2847,7 +2914,8 @@ struct Compiler
size_t oldLocals = localStack.size(); size_t oldLocals = localStack.size();
size_t oldJumps = loopJumps.size(); size_t oldJumps = loopJumps.size();
loops.push_back({oldLocals, nullptr}); loops.push_back({oldLocals, oldLocals, nullptr});
hasLoops = true;
// register layout: limit, step, index // register layout: limit, step, index
uint8_t regs = allocReg(stat, 3); uint8_t regs = allocReg(stat, 3);
@ -2911,7 +2979,8 @@ struct Compiler
size_t oldLocals = localStack.size(); size_t oldLocals = localStack.size();
size_t oldJumps = loopJumps.size(); size_t oldJumps = loopJumps.size();
loops.push_back({oldLocals, nullptr}); loops.push_back({oldLocals, oldLocals, nullptr});
hasLoops = true;
// register layout: generator, state, index, variables... // register layout: generator, state, index, variables...
uint8_t regs = allocReg(stat, 3); uint8_t regs = allocReg(stat, 3);
@ -3327,7 +3396,7 @@ struct Compiler
// before continuing, we need to close all local variables that were captured in closures since loop start // before continuing, we need to close all local variables that were captured in closures since loop start
// normally they are closed by the enclosing blocks, including the loop block, but we're skipping that here // normally they are closed by the enclosing blocks, including the loop block, but we're skipping that here
closeLocals(loops.back().localOffset); closeLocals(loops.back().localOffsetContinue);
size_t label = bytecode.emitLabel(); size_t label = bytecode.emitLabel();
@ -3640,6 +3709,9 @@ struct Compiler
{ {
Local& l = self->locals[local]; Local& l = self->locals[local];
if (FFlag::LuauCompileFixContinueValidation && l.skipped)
return;
if (!l.allocated && !undef) if (!l.allocated && !undef)
undef = local; undef = local;
} }
@ -3765,6 +3837,7 @@ struct Compiler
uint8_t reg = 0; uint8_t reg = 0;
bool allocated = false; bool allocated = false;
bool captured = false; bool captured = false;
bool skipped = false;
uint32_t debugpc = 0; uint32_t debugpc = 0;
}; };
@ -3783,6 +3856,7 @@ struct Compiler
struct Loop struct Loop
{ {
size_t localOffset; size_t localOffset;
size_t localOffsetContinue;
AstExpr* untilCondition; AstExpr* untilCondition;
}; };
@ -3830,8 +3904,10 @@ struct Compiler
const DenseHashMap<AstExprCall*, int>* builtinsFold = nullptr; const DenseHashMap<AstExprCall*, int>* builtinsFold = nullptr;
bool builtinsFoldMathK = false; bool builtinsFoldMathK = false;
// compileFunction state, gets reset for every function
unsigned int regTop = 0; unsigned int regTop = 0;
unsigned int stackSize = 0; unsigned int stackSize = 0;
bool hasLoops = false;
bool getfenvUsed = false; bool getfenvUsed = false;
bool setfenvUsed = false; bool setfenvUsed = false;
@ -3877,8 +3953,15 @@ void compileOrThrow(BytecodeBuilder& bytecode, const ParseResult& parseResult, c
// this pass analyzes mutability of locals/globals and associates locals with their initial values // this pass analyzes mutability of locals/globals and associates locals with their initial values
trackValues(compiler.globals, compiler.variables, root); trackValues(compiler.globals, compiler.variables, root);
// this visitor tracks calls to getfenv/setfenv and disables some optimizations when they are found
if (options.optimizationLevel >= 1 && (names.get("getfenv").value || names.get("setfenv").value))
{
Compiler::FenvVisitor fenvVisitor(compiler.getfenvUsed, compiler.setfenvUsed);
root->visit(&fenvVisitor);
}
// builtin folding is enabled on optimization level 2 since we can't deoptimize folding at runtime // builtin folding is enabled on optimization level 2 since we can't deoptimize folding at runtime
if (options.optimizationLevel >= 2) if (options.optimizationLevel >= 2 && (!FFlag::LuauCompileFenvNoBuiltinFold || (!compiler.getfenvUsed && !compiler.setfenvUsed)))
{ {
compiler.builtinsFold = &compiler.builtins; compiler.builtinsFold = &compiler.builtins;
@ -3898,13 +3981,6 @@ void compileOrThrow(BytecodeBuilder& bytecode, const ParseResult& parseResult, c
predictTableShapes(compiler.tableShapes, root); predictTableShapes(compiler.tableShapes, root);
} }
// this visitor tracks calls to getfenv/setfenv and disables some optimizations when they are found
if (options.optimizationLevel >= 1 && (names.get("getfenv").value || names.get("setfenv").value))
{
Compiler::FenvVisitor fenvVisitor(compiler.getfenvUsed, compiler.setfenvUsed);
root->visit(&fenvVisitor);
}
// gathers all functions with the invariant that all function references are to functions earlier in the list // gathers all functions with the invariant that all function references are to functions earlier in the list
// for example, function foo() return function() end end will result in two vector entries, [0] = anonymous and [1] = foo // for example, function foo() return function() end end will result in two vector entries, [0] = anonymous and [1] = foo
std::vector<AstExprFunction*> functions; std::vector<AstExprFunction*> functions;

View file

@ -236,7 +236,7 @@ target_sources(Luau.Analysis PRIVATE
Analysis/src/Linter.cpp Analysis/src/Linter.cpp
Analysis/src/LValue.cpp Analysis/src/LValue.cpp
Analysis/src/Module.cpp Analysis/src/Module.cpp
Analysis/src/NonStrictTypeChecker.cpp Analysis/src/NonStrictTypeChecker.cpp
Analysis/src/Normalize.cpp Analysis/src/Normalize.cpp
Analysis/src/Quantify.cpp Analysis/src/Quantify.cpp
Analysis/src/Refinement.cpp Analysis/src/Refinement.cpp
@ -400,7 +400,7 @@ if(TARGET Luau.UnitTest)
tests/LValue.test.cpp tests/LValue.test.cpp
tests/Module.test.cpp tests/Module.test.cpp
tests/NonstrictMode.test.cpp tests/NonstrictMode.test.cpp
tests/NonStrictTypeChecker.test.cpp tests/NonStrictTypeChecker.test.cpp
tests/Normalize.test.cpp tests/Normalize.test.cpp
tests/NotNull.test.cpp tests/NotNull.test.cpp
tests/Parser.test.cpp tests/Parser.test.cpp

View file

@ -2,11 +2,15 @@
// This code is based on Lua 5.x implementation licensed under MIT License; see lua_LICENSE.txt for details // This code is based on Lua 5.x implementation licensed under MIT License; see lua_LICENSE.txt for details
#include "lualib.h" #include "lualib.h"
#include "lcommon.h"
#include <string.h> #include <string.h>
#include <time.h> #include <time.h>
#define LUA_STRFTIMEOPTIONS "aAbBcdHIjmMpSUwWxXyYzZ%" #define LUA_STRFTIMEOPTIONS "aAbBcdHIjmMpSUwWxXyYzZ%"
LUAU_FASTFLAGVARIABLE(LuauOsTimegm, false)
#if defined(_WIN32) #if defined(_WIN32)
static tm* gmtime_r(const time_t* timep, tm* result) static tm* gmtime_r(const time_t* timep, tm* result)
{ {
@ -20,10 +24,49 @@ static tm* localtime_r(const time_t* timep, tm* result)
static time_t timegm(struct tm* timep) static time_t timegm(struct tm* timep)
{ {
LUAU_ASSERT(!FFlag::LuauOsTimegm);
return _mkgmtime(timep); return _mkgmtime(timep);
} }
#endif #endif
static time_t os_timegm(struct tm* timep)
{
LUAU_ASSERT(FFlag::LuauOsTimegm);
// Julian day number calculation
int day = timep->tm_mday;
int month = timep->tm_mon + 1;
int year = timep->tm_year + 1900;
// year adjustment, pretend that it starts in March
int a = timep->tm_mon % 12 < 2 ? 1 : 0;
// also adjust for out-of-range month numbers in input
a -= timep->tm_mon / 12;
int y = year + 4800 - a;
int m = month + (12 * a) - 3;
int julianday = day + ((153 * m + 2) / 5) + (365 * y) + (y / 4) - (y / 100) + (y / 400) - 32045;
const int utcstartasjulianday = 2440588; // Jan 1st 1970 offset in Julian calendar
const int64_t utcstartasjuliansecond = utcstartasjulianday * 86400ll; // same in seconds
// fail the dates before UTC start
if (julianday < utcstartasjulianday)
return time_t(-1);
int64_t daysecond = timep->tm_hour * 3600ll + timep->tm_min * 60ll + timep->tm_sec;
int64_t julianseconds = int64_t(julianday) * 86400ull + daysecond;
if (julianseconds < utcstartasjuliansecond)
return time_t(-1);
int64_t utc = julianseconds - utcstartasjuliansecond;
return time_t(utc);
}
static int os_clock(lua_State* L) static int os_clock(lua_State* L)
{ {
lua_pushnumber(L, lua_clock()); lua_pushnumber(L, lua_clock());
@ -163,7 +206,10 @@ static int os_time(lua_State* L)
ts.tm_isdst = getboolfield(L, "isdst"); ts.tm_isdst = getboolfield(L, "isdst");
// Note: upstream Lua uses mktime() here which assumes input is local time, but we prefer UTC for consistency // Note: upstream Lua uses mktime() here which assumes input is local time, but we prefer UTC for consistency
t = timegm(&ts); if (FFlag::LuauOsTimegm)
t = os_timegm(&ts);
else
t = timegm(&ts);
} }
if (t == (time_t)(-1)) if (t == (time_t)(-1))
lua_pushnil(L); lua_pushnil(L);

View file

@ -9,7 +9,7 @@
#ifndef NOMINMAX #ifndef NOMINMAX
#define NOMINMAX #define NOMINMAX
#endif #endif
#include <Windows.h> #include <windows.h>
#endif #endif
#ifdef __APPLE__ #ifdef __APPLE__

View file

@ -2061,9 +2061,8 @@ function re.type(...)
return proxy[...] and proxy[...].name; return proxy[...] and proxy[...].name;
end; end;
for k, f in pairs(re_m) do -- TODO: table.foreach is currently used as top-level loops needlessly increase native code size for this module
re[k] = f; table.foreach(re_m, function(k, f) re[k] = f end)
end;
re_m = { __index = re_m }; re_m = { __index = re_m };

View file

@ -264,6 +264,14 @@ TEST_CASE_FIXTURE(AssemblyBuilderX64Fixture, "FormsOfSetcc")
SINGLE_COMPARE(setcc(ConditionX64::BelowEqual, byte[rcx]), 0x0f, 0x96, 0x01); SINGLE_COMPARE(setcc(ConditionX64::BelowEqual, byte[rcx]), 0x0f, 0x96, 0x01);
} }
TEST_CASE_FIXTURE(AssemblyBuilderX64Fixture, "FormsOfCmov")
{
SINGLE_COMPARE(cmov(ConditionX64::LessEqual, ebx, eax), 0x0f, 0x4e, 0xd8);
SINGLE_COMPARE(cmov(ConditionX64::NotZero, rbx, qword[rax]), 0x48, 0x0f, 0x45, 0x18);
SINGLE_COMPARE(cmov(ConditionX64::Zero, rbx, qword[rax + rcx]), 0x48, 0x0f, 0x44, 0x1c, 0x08);
SINGLE_COMPARE(cmov(ConditionX64::BelowEqual, r14d, r15d), 0x45, 0x0f, 0x46, 0xf7);
}
TEST_CASE_FIXTURE(AssemblyBuilderX64Fixture, "FormsOfAbsoluteJumps") TEST_CASE_FIXTURE(AssemblyBuilderX64Fixture, "FormsOfAbsoluteJumps")
{ {
SINGLE_COMPARE(jmp(rax), 0xff, 0xe0); SINGLE_COMPARE(jmp(rax), 0xff, 0xe0);
@ -590,6 +598,7 @@ TEST_CASE("LogTest")
build.vroundsd(xmm1, xmm2, xmm3, RoundingModeX64::RoundToNearestEven); build.vroundsd(xmm1, xmm2, xmm3, RoundingModeX64::RoundToNearestEven);
build.add(rdx, qword[rcx - 12]); build.add(rdx, qword[rcx - 12]);
build.pop(r12); build.pop(r12);
build.cmov(ConditionX64::AboveEqual, rax, rbx);
build.ret(); build.ret();
build.int3(); build.int3();
@ -634,6 +643,7 @@ TEST_CASE("LogTest")
vroundsd xmm1,xmm2,xmm3,8 vroundsd xmm1,xmm2,xmm3,8
add rdx,qword ptr [rcx-0Ch] add rdx,qword ptr [rcx-0Ch]
pop r12 pop r12
cmovae rax,rbx
ret ret
int3 int3
nop nop

View file

@ -1772,6 +1772,8 @@ RETURN R0 0
TEST_CASE("LoopContinueUntil") TEST_CASE("LoopContinueUntil")
{ {
ScopedFastFlag sff("LuauCompileContinueCloseUpvals", true);
// it's valid to use locals defined inside the loop in until expression if they're defined before continue // it's valid to use locals defined inside the loop in until expression if they're defined before continue
CHECK_EQ("\n" + compileFunction0("repeat local r = math.random() if r > 0.5 then continue end r = r + 0.3 until r < 0.5"), R"( CHECK_EQ("\n" + compileFunction0("repeat local r = math.random() if r > 0.5 then continue end r = r + 0.3 until r < 0.5"), R"(
L0: GETIMPORT R0 2 [math.random] L0: GETIMPORT R0 2 [math.random]
@ -1833,17 +1835,15 @@ L2: RETURN R0 0
L0: GETIMPORT R0 2 [math.random] L0: GETIMPORT R0 2 [math.random]
CALL R0 0 1 CALL R0 0 1
LOADK R1 K3 [0.5] LOADK R1 K3 [0.5]
JUMPIFNOTLT R1 R0 L1 JUMPIFLT R1 R0 L1
CLOSEUPVALS R0 ADDK R0 R0 K4 [0.29999999999999999]
JUMP L2 L1: NEWCLOSURE R1 P0
L1: ADDK R0 R0 K4 [0.29999999999999999]
L2: NEWCLOSURE R1 P0
CAPTURE REF R0 CAPTURE REF R0
CALL R1 0 1 CALL R1 0 1
JUMPIF R1 L3 JUMPIF R1 L2
CLOSEUPVALS R0 CLOSEUPVALS R0
JUMPBACK L0 JUMPBACK L0
L3: CLOSEUPVALS R0 L2: CLOSEUPVALS R0
RETURN R0 0 RETURN R0 0
)"); )");
@ -1895,42 +1895,188 @@ L2: RETURN R0 0
L0: GETIMPORT R0 2 [math.random] L0: GETIMPORT R0 2 [math.random]
CALL R0 0 1 CALL R0 0 1
LOADK R1 K3 [0.5] LOADK R1 K3 [0.5]
JUMPIFNOTLT R1 R0 L1 JUMPIFLT R1 R0 L1
CLOSEUPVALS R0 ADDK R0 R0 K4 [0.29999999999999999]
JUMP L2 L1: NEWCLOSURE R1 P0
L1: ADDK R0 R0 K4 [0.29999999999999999]
L2: NEWCLOSURE R1 P0
CAPTURE UPVAL U0 CAPTURE UPVAL U0
CAPTURE REF R0 CAPTURE REF R0
CALL R1 0 1 CALL R1 0 1
JUMPIF R1 L3 JUMPIF R1 L2
CLOSEUPVALS R0 CLOSEUPVALS R0
JUMPBACK L0 JUMPBACK L0
L3: CLOSEUPVALS R0 L2: CLOSEUPVALS R0
RETURN R0 0 RETURN R0 0
)"); )");
} }
TEST_CASE("LoopContinueUntilOops") TEST_CASE("LoopContinueIgnoresImplicitConstant")
{ {
ScopedFastFlag luauCompileFixContinueValidation{"LuauCompileFixContinueValidation", true};
// this used to crash the compiler :( // this used to crash the compiler :(
try CHECK_EQ("\n" + compileFunction0(R"(
{
Luau::BytecodeBuilder bcb;
Luau::compileOrThrow(bcb, R"(
local _ local _
repeat repeat
continue continue
until not _ until not _
)"),
R"(
RETURN R0 0
RETURN R0 0
)"); )");
}
TEST_CASE("LoopContinueIgnoresExplicitConstant")
{
ScopedFastFlag luauCompileFixContinueValidation{"LuauCompileFixContinueValidation", true};
// Constants do not allocate locals and 'continue' validation should skip them if their lifetime already started
CHECK_EQ("\n" + compileFunction0(R"(
local c = true
repeat
continue
until c
)"),
R"(
RETURN R0 0
RETURN R0 0
)");
}
TEST_CASE("LoopContinueRespectsExplicitConstant")
{
ScopedFastFlag luauCompileFixContinueValidation{"LuauCompileFixContinueValidation", true};
// If local lifetime hasn't started, even if it's a constant that will not receive an allocation, it cannot be jumped over
try
{
Luau::BytecodeBuilder bcb;
Luau::compileOrThrow(bcb, R"(
repeat
do continue end
local c = true
until c
)");
CHECK(!"Expected CompileError");
} }
catch (Luau::CompileError& e) catch (Luau::CompileError& e)
{ {
CHECK_EQ(e.getLocation().begin.line + 1, 6);
CHECK_EQ( CHECK_EQ(
std::string(e.what()), "Local _ used in the repeat..until condition is undefined because continue statement on line 4 jumps over it"); std::string(e.what()), "Local c used in the repeat..until condition is undefined because continue statement on line 3 jumps over it");
} }
} }
TEST_CASE("LoopContinueIgnoresImplicitConstantAfterInline")
{
ScopedFastFlag luauCompileFixContinueValidation{"LuauCompileFixContinueValidation", true};
// Inlining might also replace some locals with constants instead of allocating them
CHECK_EQ("\n" + compileFunction(R"(
local function inline(f)
repeat
continue
until f
end
local function test(...)
inline(true)
end
test()
)",
1, 2),
R"(
RETURN R0 0
RETURN R0 0
)");
}
TEST_CASE("LoopContinueUntilCapture")
{
ScopedFastFlag sff("LuauCompileContinueCloseUpvals", true);
// validate continue upvalue closing behavior: continue must close locals defined in the nested scopes
// but can't close locals defined in the loop scope - these are visible to the condition and will be closed
// when evaluating the condition instead.
CHECK_EQ("\n" + compileFunction(R"(
local a a = 0
repeat
local b b = 0
if a then
local c
print(function() c = 0 end)
if a then
continue -- must close c but not a/b
end
-- must close c
end
-- must close b but not a
until function() a = 0 b = 0 end
-- must close b on loop exit
-- must close a
)",
2),
R"(
LOADNIL R0
LOADN R0 0
L0: LOADNIL R1
LOADN R1 0
JUMPIFNOT R0 L2
LOADNIL R2
GETIMPORT R3 1 [print]
NEWCLOSURE R4 P0
CAPTURE REF R2
CALL R3 1 0
JUMPIFNOT R0 L1
CLOSEUPVALS R2
JUMP L2
L1: CLOSEUPVALS R2
L2: NEWCLOSURE R2 P1
CAPTURE REF R0
CAPTURE REF R1
JUMPIF R2 L3
CLOSEUPVALS R1
JUMPBACK L0
L3: CLOSEUPVALS R1
CLOSEUPVALS R0
RETURN R0 0
)");
// a simpler version of the above test doesn't need to close anything when evaluating continue
CHECK_EQ("\n" + compileFunction(R"(
local a a = 0
repeat
local b b = 0
if a then
continue -- must not close a/b
end
-- must close b but not a
until function() a = 0 b = 0 end
-- must close b on loop exit
-- must close a
)",
1),
R"(
LOADNIL R0
LOADN R0 0
L0: LOADNIL R1
LOADN R1 0
JUMPIF R0 L1
L1: NEWCLOSURE R2 P0
CAPTURE REF R0
CAPTURE REF R1
JUMPIF R2 L2
CLOSEUPVALS R1
JUMPBACK L0
L2: CLOSEUPVALS R1
CLOSEUPVALS R0
RETURN R0 0
)");
}
TEST_CASE("AndOrOptimizations") TEST_CASE("AndOrOptimizations")
{ {
// the OR/ORK optimization triggers for cutoff since lhs is simple // the OR/ORK optimization triggers for cutoff since lhs is simple
@ -6368,7 +6514,7 @@ return
math.log10(100), math.log10(100),
math.log(1), math.log(1),
math.log(4, 2), math.log(4, 2),
math.log(27, 3), math.log(64, 4),
math.max(1, 2, 3), math.max(1, 2, 3),
math.min(1, 2, 3), math.min(1, 2, 3),
math.pow(3, 3), math.pow(3, 3),
@ -7408,4 +7554,27 @@ RETURN R0 1
)"); )");
} }
TEST_CASE("NoBuiltinFoldFenv")
{
ScopedFastFlag sff("LuauCompileFenvNoBuiltinFold", true);
// builtin folding is disabled when getfenv/setfenv is used in the module
CHECK_EQ("\n" + compileFunction(R"(
getfenv()
function test()
return math.pi, math.sin(0)
end
)",
0, 2),
R"(
GETIMPORT R0 2 [math.pi]
LOADN R2 0
FASTCALL1 24 R2 L0
GETIMPORT R1 4 [math.sin]
CALL R1 1 1
L0: RETURN R0 2
)");
}
TEST_SUITE_END(); TEST_SUITE_END();

View file

@ -10,6 +10,7 @@
#include "Luau/TypeInfer.h" #include "Luau/TypeInfer.h"
#include "Luau/BytecodeBuilder.h" #include "Luau/BytecodeBuilder.h"
#include "Luau/Frontend.h" #include "Luau/Frontend.h"
#include "Luau/CodeGen.h"
#include "doctest.h" #include "doctest.h"
#include "ScopedFlags.h" #include "ScopedFlags.h"
@ -224,7 +225,7 @@ static StateRef runConformance(const char* name, void (*setup)(lua_State* L) = n
free(bytecode); free(bytecode);
if (result == 0 && codegen && !skipCodegen && luau_codegen_supported()) if (result == 0 && codegen && !skipCodegen && luau_codegen_supported())
luau_codegen_compile(L, -1); Luau::CodeGen::compile(L, -1, Luau::CodeGen::CodeGen_ColdFunctions);
int status = (result == 0) ? lua_resume(L, nullptr, 0) : LUA_ERRSYNTAX; int status = (result == 0) ? lua_resume(L, nullptr, 0) : LUA_ERRSYNTAX;
@ -288,7 +289,7 @@ TEST_CASE("Assert")
TEST_CASE("Basic") TEST_CASE("Basic")
{ {
ScopedFastFlag sffs{"LuauFloorDivision", true}; ScopedFastFlag sffs{"LuauFloorDivision", true};
ScopedFastFlag sfff{"LuauImproveForN", true}; ScopedFastFlag sfff{"LuauImproveForN2", true};
runConformance("basic.lua"); runConformance("basic.lua");
} }
@ -379,6 +380,8 @@ TEST_CASE("Events")
TEST_CASE("Constructs") TEST_CASE("Constructs")
{ {
ScopedFastFlag sff("LuauCompileContinueCloseUpvals", true);
runConformance("constructs.lua"); runConformance("constructs.lua");
} }
@ -1809,8 +1812,6 @@ TEST_CASE("Native")
TEST_CASE("NativeTypeAnnotations") TEST_CASE("NativeTypeAnnotations")
{ {
ScopedFastFlag bytecodeVersion4("BytecodeVersion4", true);
// This tests requires code to run natively, otherwise all 'is_native' checks will fail // This tests requires code to run natively, otherwise all 'is_native' checks will fail
if (!codegen || !luau_codegen_supported()) if (!codegen || !luau_codegen_supported())
return; return;
@ -1891,7 +1892,7 @@ TEST_CASE("HugeFunction")
REQUIRE(result == 0); REQUIRE(result == 0);
if (codegen && luau_codegen_supported()) if (codegen && luau_codegen_supported())
luau_codegen_compile(L, -1); Luau::CodeGen::compile(L, -1, Luau::CodeGen::CodeGen_ColdFunctions);
int status = lua_resume(L, nullptr, 0); int status = lua_resume(L, nullptr, 0);
REQUIRE(status == 0); REQUIRE(status == 0);

View file

@ -3,10 +3,28 @@
#include "Fixture.h" #include "Fixture.h"
#include "Luau/Common.h"
#include "Luau/Ast.h"
#include "Luau/ModuleResolver.h"
#include "ScopedFlags.h"
#include "doctest.h" #include "doctest.h"
#include <iostream>
using namespace Luau; using namespace Luau;
struct NonStrictTypeCheckerFixture : Fixture
{
ParseResult parse(std::string source)
{
ParseOptions opts;
opts.allowDeclarationSyntax = true;
ScopedFastFlag sff{"LuauCheckedFunctionSyntax", true};
return tryParse(source, opts);
}
};
TEST_SUITE_BEGIN("NonStrictTypeCheckerTest"); TEST_SUITE_BEGIN("NonStrictTypeCheckerTest");
TEST_CASE_FIXTURE(Fixture, "basic") TEST_CASE_FIXTURE(Fixture, "basic")
@ -14,4 +32,85 @@ TEST_CASE_FIXTURE(Fixture, "basic")
Luau::checkNonStrict(builtinTypes, nullptr); Luau::checkNonStrict(builtinTypes, nullptr);
} }
TEST_CASE_FIXTURE(NonStrictTypeCheckerFixture, "parse_top_level_checked_fn")
{
std::string src = R"BUILTIN_SRC(
declare function @checked abs(n: number): number
)BUILTIN_SRC";
ParseResult pr = parse(src);
LUAU_ASSERT(pr.errors.size() == 0);
LUAU_ASSERT(pr.root->body.size == 1);
AstStat* root = *(pr.root->body.data);
auto func = root->as<AstStatDeclareFunction>();
LUAU_ASSERT(func);
LUAU_ASSERT(func->checkedFunction);
}
TEST_CASE_FIXTURE(NonStrictTypeCheckerFixture, "parse_declared_table_checked_member")
{
std::string src = R"BUILTIN_SRC(
declare math : {
abs : @checked (number) -> number
}
)BUILTIN_SRC";
ParseResult pr = parse(src);
LUAU_ASSERT(pr.errors.size() == 0);
LUAU_ASSERT(pr.root->body.size == 1);
AstStat* root = *(pr.root->body.data);
auto glob = root->as<AstStatDeclareGlobal>();
LUAU_ASSERT(glob);
auto tbl = glob->type->as<AstTypeTable>();
LUAU_ASSERT(tbl);
LUAU_ASSERT(tbl->props.size == 1);
auto prop = *tbl->props.data;
auto func = prop.type->as<AstTypeFunction>();
LUAU_ASSERT(func);
LUAU_ASSERT(func->checkedFunction);
}
TEST_CASE_FIXTURE(NonStrictTypeCheckerFixture, "parse_checked_outside_decl_fails")
{
auto src = R"(
local @checked = 3
)";
ParseResult pr = parse(src);
LUAU_ASSERT(pr.errors.size() > 0);
auto ts = pr.errors[1].getMessage();
}
TEST_CASE_FIXTURE(NonStrictTypeCheckerFixture, "parse_checked_in_and_out_of_decl_fails")
{
auto src = R"(
local @checked = 3
declare function @checked abs(n: number): number
)";
auto pr = parse(src);
LUAU_ASSERT(pr.errors.size() == 2);
LUAU_ASSERT(pr.errors[0].getLocation().begin.line == 1);
LUAU_ASSERT(pr.errors[1].getLocation().begin.line == 1);
}
TEST_CASE_FIXTURE(NonStrictTypeCheckerFixture, "parse_checked_as_function_name_fails")
{
auto pr = parse(R"(
function @checked(x: number) : number
end
)");
LUAU_ASSERT(pr.errors.size() > 0);
}
TEST_CASE_FIXTURE(NonStrictTypeCheckerFixture, "cannot_use_@_as_variable_name")
{
auto pr = parse(R"(
local @blah = 3
)");
LUAU_ASSERT(pr.errors.size() > 0);
}
TEST_SUITE_END(); TEST_SUITE_END();

View file

@ -598,6 +598,20 @@ TEST_CASE_FIXTURE(SubtypeFixture, "(number) -> () <!: <T>(T) -> ()")
CHECK_IS_NOT_SUBTYPE(numberToNothingType, genericTToNothingType); CHECK_IS_NOT_SUBTYPE(numberToNothingType, genericTToNothingType);
} }
TEST_CASE_FIXTURE(SubtypeFixture, "<T>() -> (T, T) <!: () -> (string, number)")
{
TypeId nothingToTwoTs = arena.addType(FunctionType{
{genericT},
{},
builtinTypes->emptyTypePack,
arena.addTypePack({genericT, genericT})
});
TypeId nothingToStringAndNumber = fn({}, {builtinTypes->stringType, builtinTypes->numberType});
CHECK_IS_NOT_SUBTYPE(nothingToTwoTs, nothingToStringAndNumber);
}
TEST_CASE_FIXTURE(SubtypeFixture, "<A...>(A...) -> A... <: (number) -> number") TEST_CASE_FIXTURE(SubtypeFixture, "<A...>(A...) -> A... <: (number) -> number")
{ {
CHECK_IS_SUBTYPE(genericAsToAsType, numberToNumberType); CHECK_IS_SUBTYPE(genericAsToAsType, numberToNumberType);

View file

@ -2,6 +2,7 @@
#include "Luau/TypeFamily.h" #include "Luau/TypeFamily.h"
#include "Luau/ConstraintSolver.h" #include "Luau/ConstraintSolver.h"
#include "Luau/NotNull.h"
#include "Luau/TxnLog.h" #include "Luau/TxnLog.h"
#include "Luau/Type.h" #include "Luau/Type.h"
@ -22,21 +23,20 @@ struct FamilyFixture : Fixture
{ {
swapFamily = TypeFamily{/* name */ "Swap", swapFamily = TypeFamily{/* name */ "Swap",
/* reducer */ /* reducer */
[](std::vector<TypeId> tys, std::vector<TypePackId> tps, NotNull<TypeArena> arena, NotNull<BuiltinTypes> builtins, [](std::vector<TypeId> tys, std::vector<TypePackId> tps, NotNull<TypeFamilyContext> ctx) -> TypeFamilyReductionResult<TypeId> {
NotNull<const TxnLog> log, NotNull<Scope> scope, NotNull<Normalizer> normalizer, ConstraintSolver* solver) -> TypeFamilyReductionResult<TypeId> {
LUAU_ASSERT(tys.size() == 1); LUAU_ASSERT(tys.size() == 1);
TypeId param = log->follow(tys.at(0)); TypeId param = follow(tys.at(0));
if (isString(param)) if (isString(param))
{ {
return TypeFamilyReductionResult<TypeId>{builtins->numberType, false, {}, {}}; return TypeFamilyReductionResult<TypeId>{ctx->builtins->numberType, false, {}, {}};
} }
else if (isNumber(param)) else if (isNumber(param))
{ {
return TypeFamilyReductionResult<TypeId>{builtins->stringType, false, {}, {}}; return TypeFamilyReductionResult<TypeId>{ctx->builtins->stringType, false, {}, {}};
} }
else if (log->get<BlockedType>(param) || log->get<PendingExpansionType>(param) || else if (is<BlockedType>(param) || is<PendingExpansionType>(param) || is<TypeFamilyInstanceType>(param) ||
log->get<TypeFamilyInstanceType>(param) || (solver && solver->hasUnresolvedConstraints(param))) (ctx->solver && ctx->solver->hasUnresolvedConstraints(param)))
{ {
return TypeFamilyReductionResult<TypeId>{std::nullopt, false, {param}, {}}; return TypeFamilyReductionResult<TypeId>{std::nullopt, false, {param}, {}};
} }

View file

@ -198,10 +198,7 @@ TEST_CASE_FIXTURE(Fixture, "generic_aliases")
)"); )");
LUAU_REQUIRE_ERROR_COUNT(1, result); LUAU_REQUIRE_ERROR_COUNT(1, result);
const std::string expected = R"(Type 'bad' could not be converted into 'T<number>' const std::string expected = "Type 'bad' could not be converted into 'T<number>'";
caused by:
Property 'v' is not compatible.
Type 'string' could not be converted into 'number' in an invariant context)";
CHECK(result.errors[0].location == Location{{4, 31}, {4, 44}}); CHECK(result.errors[0].location == Location{{4, 31}, {4, 44}});
CHECK_EQ(expected, toString(result.errors[0])); CHECK_EQ(expected, toString(result.errors[0]));
} }
@ -220,13 +217,7 @@ TEST_CASE_FIXTURE(Fixture, "dependent_generic_aliases")
)"); )");
LUAU_REQUIRE_ERROR_COUNT(1, result); LUAU_REQUIRE_ERROR_COUNT(1, result);
const std::string expected = R"(Type 'bad' could not be converted into 'U<number>' const std::string expected = "Type 'bad' could not be converted into 'U<number>'";
caused by:
Property 't' is not compatible.
Type '{| v: string |}' could not be converted into 'T<number>'
caused by:
Property 'v' is not compatible.
Type 'string' could not be converted into 'number' in an invariant context)";
CHECK(result.errors[0].location == Location{{4, 31}, {4, 52}}); CHECK(result.errors[0].location == Location{{4, 31}, {4, 52}});
CHECK_EQ(expected, toString(result.errors[0])); CHECK_EQ(expected, toString(result.errors[0]));

View file

@ -997,7 +997,7 @@ TEST_CASE_FIXTURE(Fixture, "cli_80596_simplify_degenerate_intersections")
local x: number = obj.x or 3 local x: number = obj.x or 3
)"); )");
LUAU_REQUIRE_NO_ERRORS(result); LUAU_REQUIRE_ERRORS(result);
} }
TEST_CASE_FIXTURE(Fixture, "cli_80596_simplify_more_realistic_intersections") TEST_CASE_FIXTURE(Fixture, "cli_80596_simplify_more_realistic_intersections")
@ -1023,7 +1023,7 @@ TEST_CASE_FIXTURE(Fixture, "cli_80596_simplify_more_realistic_intersections")
local x: number = obj.x or 3 local x: number = obj.x or 3
)"); )");
LUAU_REQUIRE_NO_ERRORS(result); LUAU_REQUIRE_ERRORS(result);
} }
TEST_SUITE_END(); TEST_SUITE_END();

View file

@ -936,6 +936,128 @@ TEST_CASE_FIXTURE(Fixture, "infer_any_in_all_modes_when_lhs_is_unknown")
// the case right now, though. // the case right now, though.
} }
TEST_CASE_FIXTURE(Fixture, "infer_type_for_generic_subtraction")
{
CheckResult result = check(Mode::Strict, R"(
local function f(x, y)
return x - y
end
)");
if (FFlag::DebugLuauDeferredConstraintResolution)
{
LUAU_REQUIRE_NO_ERRORS(result);
CHECK(toString(requireType("f")) == "<a, b>(a, b) -> Sub<a, b>");
}
else
{
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(toString(result.errors[0]), "Unknown type used in - operation; consider adding a type annotation to 'x'");
}
}
TEST_CASE_FIXTURE(Fixture, "infer_type_for_generic_multiplication")
{
CheckResult result = check(Mode::Strict, R"(
local function f(x, y)
return x * y
end
)");
if (FFlag::DebugLuauDeferredConstraintResolution)
{
LUAU_REQUIRE_NO_ERRORS(result);
CHECK(toString(requireType("f")) == "<a, b>(a, b) -> Mul<a, b>");
}
else
{
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(toString(result.errors[0]), "Unknown type used in * operation; consider adding a type annotation to 'x'");
}
}
TEST_CASE_FIXTURE(Fixture, "infer_type_for_generic_division")
{
CheckResult result = check(Mode::Strict, R"(
local function f(x, y)
return x / y
end
)");
if (FFlag::DebugLuauDeferredConstraintResolution)
{
LUAU_REQUIRE_NO_ERRORS(result);
CHECK(toString(requireType("f")) == "<a, b>(a, b) -> Div<a, b>");
}
else
{
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(toString(result.errors[0]), "Unknown type used in / operation; consider adding a type annotation to 'x'");
}
}
TEST_CASE_FIXTURE(Fixture, "infer_type_for_generic_floor_division")
{
ScopedFastFlag floorDiv{"LuauFloorDivision", true};
CheckResult result = check(Mode::Strict, R"(
local function f(x, y)
return x // y
end
)");
if (FFlag::DebugLuauDeferredConstraintResolution)
{
LUAU_REQUIRE_NO_ERRORS(result);
CHECK(toString(requireType("f")) == "<a, b>(a, b) -> FloorDiv<a, b>");
}
else
{
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(toString(result.errors[0]), "Unknown type used in // operation; consider adding a type annotation to 'x'");
}
}
TEST_CASE_FIXTURE(Fixture, "infer_type_for_generic_exponentiation")
{
CheckResult result = check(Mode::Strict, R"(
local function f(x, y)
return x ^ y
end
)");
if (FFlag::DebugLuauDeferredConstraintResolution)
{
LUAU_REQUIRE_NO_ERRORS(result);
CHECK(toString(requireType("f")) == "<a, b>(a, b) -> Exp<a, b>");
}
else
{
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(toString(result.errors[0]), "Unknown type used in ^ operation; consider adding a type annotation to 'x'");
}
}
TEST_CASE_FIXTURE(Fixture, "infer_type_for_generic_modulo")
{
CheckResult result = check(Mode::Strict, R"(
local function f(x, y)
return x % y
end
)");
if (FFlag::DebugLuauDeferredConstraintResolution)
{
LUAU_REQUIRE_NO_ERRORS(result);
CHECK(toString(requireType("f")) == "<a, b>(a, b) -> Mod<a, b>");
}
else
{
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(toString(result.errors[0]), "Unknown type used in % operation; consider adding a type annotation to 'x'");
}
}
TEST_CASE_FIXTURE(BuiltinsFixture, "equality_operations_succeed_if_any_union_branch_succeeds") TEST_CASE_FIXTURE(BuiltinsFixture, "equality_operations_succeed_if_any_union_branch_succeeds")
{ {
CheckResult result = check(R"( CheckResult result = check(R"(

View file

@ -1942,4 +1942,54 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "conditional_refinement_should_stay_error_sup
LUAU_REQUIRE_NO_ERRORS(result); LUAU_REQUIRE_NO_ERRORS(result);
} }
TEST_CASE_FIXTURE(BuiltinsFixture, "globals_can_be_narrowed_too")
{
CheckResult result = check(R"(
if typeof(string) == 'string' then
local foo = string
end
)");
CHECK("never" == toString(requireTypeAtPosition(Position{2, 24})));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "luau_polyfill_isindexkey_refine_conjunction")
{
CheckResult result = check(R"(
local function isIndexKey(k, contiguousLength)
return type(k) == "number"
and k <= contiguousLength -- nothing out of bounds
and 1 <= k -- nothing illegal for array indices
and math.floor(k) == k -- no float keys
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(BuiltinsFixture, "luau_polyfill_isindexkey_refine_conjunction_variant")
{
CheckResult result = check(R"(
local function isIndexKey(k, contiguousLength: number)
return type(k) == "number"
and k <= contiguousLength -- nothing out of bounds
and 1 <= k -- nothing illegal for array indices
and math.floor(k) == k -- no float keys
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(BuiltinsFixture, "globals_can_be_narrowed_too")
{
CheckResult result = check(R"(
if typeof(string) == 'string' then
local foo = string
end
)");
CHECK("never" == toString(requireTypeAtPosition(Position{2, 24})));
}
TEST_SUITE_END(); TEST_SUITE_END();

View file

@ -366,10 +366,7 @@ TEST_CASE_FIXTURE(Fixture, "parametric_tagged_union_alias")
LUAU_REQUIRE_ERROR_COUNT(1, result); LUAU_REQUIRE_ERROR_COUNT(1, result);
const std::string expectedError = R"(Type 'a' could not be converted into 'Err<number> | Ok<string>' const std::string expectedError = "Type 'a' could not be converted into 'Err<number> | Ok<string>'";
caused by:
None of the union options are compatible. For example:
Table type 'a' not compatible with type 'Err<number>' because the former is missing field 'error')";
CHECK(toString(result.errors[0]) == expectedError); CHECK(toString(result.errors[0]) == expectedError);
} }

View file

@ -2672,7 +2672,7 @@ TEST_CASE_FIXTURE(Fixture, "generalize_table_argument")
std::optional<TypeId> fooArg1 = first(fooType->argTypes); std::optional<TypeId> fooArg1 = first(fooType->argTypes);
REQUIRE(fooArg1); REQUIRE(fooArg1);
const TableType* fooArg1Table = get<TableType>(*fooArg1); const TableType* fooArg1Table = get<TableType>(follow(*fooArg1));
REQUIRE(fooArg1Table); REQUIRE(fooArg1Table);
CHECK_EQ(fooArg1Table->state, TableState::Generic); CHECK_EQ(fooArg1Table->state, TableState::Generic);

View file

@ -237,4 +237,19 @@ repeat
i = i+1 i = i+1
until i==c until i==c
-- validate continue upvalue close behavior
local function check_connected(writer, reader)
writer(1)
assert(reader() == 1)
return true
end
repeat
local value = nil
local function write(n)
value = n
end
continue
until check_connected(write, function() return value end)
return 'OK' return 'OK'

View file

@ -23,6 +23,29 @@ assert(os.time({ year = 1970, month = 1, day = 1, hour = 0, min = 0, sec = 0}) =
assert(os.time({ year = 3000, month = 12, day = 31, hour = 23, min = 59, sec = 59}) == 32535215999) -- just before Windows max range assert(os.time({ year = 3000, month = 12, day = 31, hour = 23, min = 59, sec = 59}) == 32535215999) -- just before Windows max range
assert(os.time({ year = 1970, month = 1, day = 1, hour = 0, min = 0, sec = -1}) == nil) -- going before using time fields assert(os.time({ year = 1970, month = 1, day = 1, hour = 0, min = 0, sec = -1}) == nil) -- going before using time fields
assert(os.time({ year = 2000, month = 6, day = 10, hour = 0, min = 0, sec = 0}) == 960595200)
assert(os.time({ year = 2000, month = 6, day = 10, hour = 0, min = 0, sec = -86400}) == 960508800)
assert(os.time({ year = 2000, month = 6, day = 10, hour = 0, min = 0, sec = 86400}) == 960681600)
assert(os.time({ year = 2000, month = 6, day = 10, hour = 0, min = -600, sec = 0}) == 960559200)
assert(os.time({ year = 2000, month = 6, day = 10, hour = 0, min = 600, sec = 0}) == 960631200)
assert(os.time({ year = 2000, month = 6, day = 10, hour = -600, min = 0, sec = 0}) == 958435200)
assert(os.time({ year = 2000, month = 6, day = 10, hour = 600, min = 0, sec = 0}) == 962755200)
assert(os.time({ year = 2000, month = 6, day = -100, hour = 0, min = 0, sec = 0}) == 951091200)
assert(os.time({ year = 2000, month = 6, day = 1000, hour = 0, min = 0, sec = 0}) == 1046131200)
assert(os.time({ year = 2000, month = -60, day = 10, hour = 0, min = 0, sec = 0}) == 787017600)
assert(os.time({ year = 2000, month = 60, day = 10, hour = 0, min = 0, sec = 0}) == 1102636800)
assert(os.time({ year = 2000, month = 6, day = 10, hour = 0, min = 0, sec = -86400000}) == 874195200)
assert(os.time({ year = 2000, month = 6, day = 10, hour = 0, min = 0, sec = 86400000}) == 1046995200)
assert(os.time({ year = 2000, month = 6, day = 10, hour = 0, min = -600000, sec = 0}) == 924595200)
assert(os.time({ year = 2000, month = 6, day = 10, hour = 0, min = 600000, sec = 0}) == 996595200)
assert(os.time({ year = 2100, month = 6, day = 10, hour = -600000, min = 0, sec = 0}) == 1956268800)
assert(os.time({ year = 2100, month = 6, day = 10, hour = 600000, min = 0, sec = 0}) == 6276268800)
assert(os.time({ year = 2100, month = 6, day = -10000, hour = 0, min = 0, sec = 0}) == 3251404800)
assert(os.time({ year = 2100, month = 6, day = 100000, hour = 0, min = 0, sec = 0}) == 12755404800)
assert(os.time({ year = 2100, month = -600, day = 10, hour = 0, min = 0, sec = 0}) == 2522707200)
assert(os.time({ year = 2100, month = 600, day = 10, hour = 0, min = 0, sec = 0}) == 5678380800)
local function checkDateTable (t) local function checkDateTable (t)
local D = os.date("!*t", t) local D = os.date("!*t", t)
assert(os.time(D) == t) assert(os.time(D) == t)

View file

@ -346,7 +346,7 @@ assert(math.log10("10") == 1)
assert(math.log("0") == -inf) assert(math.log("0") == -inf)
assert(math.log("8", 2) == 3) assert(math.log("8", 2) == 3)
assert(math.log("10", 10) == 1) assert(math.log("10", 10) == 1)
assert(math.log("9", 3) == 2) assert(math.log("16", 4) == 2)
assert(math.max("1", 2) == 2) assert(math.max("1", 2) == 2)
assert(math.max(2, "1") == 2) assert(math.max(2, "1") == 2)
assert(math.max(1, 2, "3") == 3) assert(math.max(1, 2, "3") == 3)

View file

@ -203,6 +203,33 @@ local function arraySizeOpt2(a, i)
return a[i] + a[5] return a[i] + a[5]
end end
assert(arraySizeOpt1({1}, 1) == 71) assert(arraySizeOpt2({1}, 1) == 71)
function deadLoopBody(n)
local r = 0
if n and false then
for i = 1, n do
r += 1
end
end
return r
end
assert(deadLoopBody(5) == 0)
function arrayIndexingSpecialNumbers1(a, b, c)
local arr = table.create(100000)
arr[a] = 9
arr[b-1] = 80
arr[b] = 700
arr[b+1] = 6000
arr[c-1] = 50000
arr[c] = 400000
arr[c+1] = 3000000
return arr[1] + arr[255] + arr[256] + arr[257] + arr[65535] + arr[65536] + arr[65537]
end
assert(arrayIndexingSpecialNumbers1(1, 256, 65536) == 3456789)
return('OK') return('OK')

View file

@ -24,6 +24,8 @@ assert(tostring(-0.17) == "-0.17")
assert(tostring(math.pi) == "3.141592653589793") assert(tostring(math.pi) == "3.141592653589793")
-- fuzzing corpus -- fuzzing corpus
-- Note: If the assert below fires it may indicate floating point denormalized values
-- are not handled as expected.
assert(tostring(5.4536123983019448e-311) == "5.453612398302e-311") assert(tostring(5.4536123983019448e-311) == "5.453612398302e-311")
assert(tostring(5.4834368411298348e-311) == "5.48343684113e-311") assert(tostring(5.4834368411298348e-311) == "5.48343684113e-311")
assert(tostring(4.4154895841930002e-305) == "4.415489584193e-305") assert(tostring(4.4154895841930002e-305) == "4.415489584193e-305")

View file

@ -15,7 +15,11 @@
#ifndef NOMINMAX #ifndef NOMINMAX
#define NOMINMAX #define NOMINMAX
#endif #endif
#include <Windows.h> // IsDebuggerPresent #include <windows.h> // IsDebuggerPresent
#endif
#if defined(__x86_64__) || defined(_M_X64)
#include <immintrin.h>
#endif #endif
#ifdef __APPLE__ #ifdef __APPLE__
@ -253,8 +257,21 @@ static void setFastFlags(const std::vector<doctest::String>& flags)
} }
} }
// This function performs system/architecture specific initialization prior to running tests.
static void initSystem()
{
#if defined(__x86_64__) || defined(_M_X64)
// Some unit tests make use of denormalized numbers. So flags to flush to zero or treat denormals as zero
// must be disabled for expected behavior.
_MM_SET_FLUSH_ZERO_MODE(_MM_FLUSH_ZERO_OFF);
_MM_SET_DENORMALS_ZERO_MODE(_MM_DENORMALS_ZERO_OFF);
#endif
}
int main(int argc, char** argv) int main(int argc, char** argv)
{ {
initSystem();
Luau::assertHandler() = testAssertionHandler; Luau::assertHandler() = testAssertionHandler;

View file

@ -1,4 +1,6 @@
AnnotationTests.infer_type_of_value_a_via_typeof_with_assignment AnnotationTests.infer_type_of_value_a_via_typeof_with_assignment
AnnotationTests.two_type_params
AnnotationTests.use_generic_type_alias
AstQuery.last_argument_function_call_type AstQuery.last_argument_function_call_type
AstQuery::getDocumentationSymbolAtPosition.table_overloaded_function_prop AstQuery::getDocumentationSymbolAtPosition.table_overloaded_function_prop
AutocompleteTest.anonymous_autofilled_generic_on_argument_type_pack_vararg AutocompleteTest.anonymous_autofilled_generic_on_argument_type_pack_vararg
@ -24,6 +26,7 @@ AutocompleteTest.type_correct_suggestion_in_argument
AutocompleteTest.unsealed_table_2 AutocompleteTest.unsealed_table_2
BuiltinTests.aliased_string_format BuiltinTests.aliased_string_format
BuiltinTests.assert_removes_falsy_types BuiltinTests.assert_removes_falsy_types
BuiltinTests.assert_removes_falsy_types2
BuiltinTests.assert_removes_falsy_types_even_from_type_pack_tail_but_only_for_the_first_type BuiltinTests.assert_removes_falsy_types_even_from_type_pack_tail_but_only_for_the_first_type
BuiltinTests.assert_returns_false_and_string_iff_it_knows_the_first_argument_cannot_be_truthy BuiltinTests.assert_returns_false_and_string_iff_it_knows_the_first_argument_cannot_be_truthy
BuiltinTests.bad_select_should_not_crash BuiltinTests.bad_select_should_not_crash
@ -39,6 +42,7 @@ BuiltinTests.gmatch_capture_types_set_containing_lbracket
BuiltinTests.gmatch_definition BuiltinTests.gmatch_definition
BuiltinTests.ipairs_iterator_should_infer_types_and_type_check BuiltinTests.ipairs_iterator_should_infer_types_and_type_check
BuiltinTests.next_iterator_should_infer_types_and_type_check BuiltinTests.next_iterator_should_infer_types_and_type_check
BuiltinTests.os_time_takes_optional_date_table
BuiltinTests.pairs_iterator_should_infer_types_and_type_check BuiltinTests.pairs_iterator_should_infer_types_and_type_check
BuiltinTests.see_thru_select BuiltinTests.see_thru_select
BuiltinTests.see_thru_select_count BuiltinTests.see_thru_select_count
@ -47,6 +51,7 @@ BuiltinTests.select_way_out_of_range
BuiltinTests.select_with_decimal_argument_is_rounded_down BuiltinTests.select_with_decimal_argument_is_rounded_down
BuiltinTests.set_metatable_needs_arguments BuiltinTests.set_metatable_needs_arguments
BuiltinTests.setmetatable_should_not_mutate_persisted_types BuiltinTests.setmetatable_should_not_mutate_persisted_types
BuiltinTests.sort
BuiltinTests.sort_with_bad_predicate BuiltinTests.sort_with_bad_predicate
BuiltinTests.sort_with_predicate BuiltinTests.sort_with_predicate
BuiltinTests.string_format_arg_count_mismatch BuiltinTests.string_format_arg_count_mismatch
@ -54,6 +59,7 @@ BuiltinTests.string_format_as_method
BuiltinTests.string_format_correctly_ordered_types BuiltinTests.string_format_correctly_ordered_types
BuiltinTests.string_format_report_all_type_errors_at_correct_positions BuiltinTests.string_format_report_all_type_errors_at_correct_positions
BuiltinTests.string_format_use_correct_argument2 BuiltinTests.string_format_use_correct_argument2
BuiltinTests.table_concat_returns_string
BuiltinTests.table_dot_remove_optionally_returns_generic BuiltinTests.table_dot_remove_optionally_returns_generic
BuiltinTests.table_freeze_is_generic BuiltinTests.table_freeze_is_generic
BuiltinTests.table_insert_correctly_infers_type_of_array_2_args_overload BuiltinTests.table_insert_correctly_infers_type_of_array_2_args_overload
@ -90,92 +96,181 @@ ControlFlowAnalysis.type_alias_does_not_leak_out_continuing
DefinitionTests.class_definition_indexer DefinitionTests.class_definition_indexer
DefinitionTests.class_definition_overload_metamethods DefinitionTests.class_definition_overload_metamethods
DefinitionTests.class_definition_string_props DefinitionTests.class_definition_string_props
DefinitionTests.declaring_generic_functions
DefinitionTests.definition_file_classes DefinitionTests.definition_file_classes
Differ.equal_generictp_cyclic Differ.equal_generictp_cyclic
Differ.equal_table_A_B_C
Differ.equal_table_cyclic_diamonds_unraveled Differ.equal_table_cyclic_diamonds_unraveled
Differ.equal_table_kind_A
Differ.equal_table_kind_B
Differ.equal_table_kind_C
Differ.equal_table_kind_D
Differ.equal_table_measuring_tapes
Differ.equal_table_two_cyclic_tables_are_not_different
Differ.equal_table_two_shifted_circles_are_not_different Differ.equal_table_two_shifted_circles_are_not_different
Differ.generictp_normal Differ.generictp_normal
Differ.generictp_normal_2 Differ.generictp_normal_2
Differ.left_cyclic_table_right_table_missing_property Differ.left_cyclic_table_right_table_missing_property
Differ.left_cyclic_table_right_table_property_wrong Differ.left_cyclic_table_right_table_property_wrong
Differ.negation
Differ.right_cyclic_table_left_table_missing_property
Differ.right_cyclic_table_left_table_property_wrong Differ.right_cyclic_table_left_table_property_wrong
Differ.table_left_circle_right_measuring_tape Differ.table_left_circle_right_measuring_tape
FrontendTest.accumulate_cached_errors_in_consistent_order
FrontendTest.it_should_be_safe_to_stringify_errors_when_full_type_graph_is_discarded
GenericsTests.apply_type_function_nested_generics1
GenericsTests.apply_type_function_nested_generics3
GenericsTests.better_mismatch_error_messages GenericsTests.better_mismatch_error_messages
GenericsTests.bound_tables_do_not_clone_original_fields
GenericsTests.check_generic_function
GenericsTests.check_generic_local_function
GenericsTests.check_generic_typepack_function GenericsTests.check_generic_typepack_function
GenericsTests.check_mutual_generic_functions GenericsTests.check_mutual_generic_functions
GenericsTests.check_nested_generic_function
GenericsTests.check_recursive_generic_function
GenericsTests.correctly_instantiate_polymorphic_member_functions GenericsTests.correctly_instantiate_polymorphic_member_functions
GenericsTests.do_not_always_instantiate_generic_intersection_types
GenericsTests.do_not_infer_generic_functions GenericsTests.do_not_infer_generic_functions
GenericsTests.dont_substitute_bound_types GenericsTests.dont_substitute_bound_types
GenericsTests.error_detailed_function_mismatch_generic_pack
GenericsTests.error_detailed_function_mismatch_generic_types
GenericsTests.factories_of_generics
GenericsTests.function_arguments_can_be_polytypes
GenericsTests.generic_argument_count_too_few GenericsTests.generic_argument_count_too_few
GenericsTests.generic_argument_count_too_many GenericsTests.generic_argument_count_too_many
GenericsTests.generic_factories
GenericsTests.generic_functions_dont_cache_type_parameters GenericsTests.generic_functions_dont_cache_type_parameters
GenericsTests.generic_functions_in_types
GenericsTests.generic_functions_should_be_memory_safe GenericsTests.generic_functions_should_be_memory_safe
GenericsTests.generic_type_pack_parentheses GenericsTests.generic_type_pack_parentheses
GenericsTests.generic_type_pack_unification1
GenericsTests.generic_type_pack_unification2
GenericsTests.generic_type_pack_unification3
GenericsTests.higher_rank_polymorphism_should_not_accept_instantiated_arguments GenericsTests.higher_rank_polymorphism_should_not_accept_instantiated_arguments
GenericsTests.hof_subtype_instantiation_regression GenericsTests.hof_subtype_instantiation_regression
GenericsTests.infer_generic_function
GenericsTests.infer_generic_function_function_argument GenericsTests.infer_generic_function_function_argument
GenericsTests.infer_generic_function_function_argument_2 GenericsTests.infer_generic_function_function_argument_2
GenericsTests.infer_generic_function_function_argument_3 GenericsTests.infer_generic_function_function_argument_3
GenericsTests.infer_generic_function_function_argument_overloaded GenericsTests.infer_generic_function_function_argument_overloaded
GenericsTests.infer_generic_lib_function_function_argument GenericsTests.infer_generic_lib_function_function_argument
GenericsTests.infer_generic_local_function
GenericsTests.infer_generic_property GenericsTests.infer_generic_property
GenericsTests.infer_nested_generic_function
GenericsTests.inferred_local_vars_can_be_polytypes
GenericsTests.instantiated_function_argument_names GenericsTests.instantiated_function_argument_names
GenericsTests.mutable_state_polymorphism GenericsTests.instantiation_sharing_types
GenericsTests.local_vars_can_be_polytypes
GenericsTests.no_stack_overflow_from_quantifying GenericsTests.no_stack_overflow_from_quantifying
GenericsTests.properties_can_be_instantiated_polytypes
GenericsTests.properties_can_be_polytypes GenericsTests.properties_can_be_polytypes
GenericsTests.quantify_functions_even_if_they_have_an_explicit_generic GenericsTests.quantify_functions_even_if_they_have_an_explicit_generic
GenericsTests.rank_N_types_via_typeof
GenericsTests.self_recursive_instantiated_param GenericsTests.self_recursive_instantiated_param
GenericsTests.type_parameters_can_be_polytypes
GenericsTests.typefuns_sharing_types
IntersectionTypes.error_detailed_intersection_all
IntersectionTypes.error_detailed_intersection_part
IntersectionTypes.intersect_bool_and_false
IntersectionTypes.intersect_false_and_bool_and_false
IntersectionTypes.intersect_metatables
IntersectionTypes.intersect_saturate_overloaded_functions
IntersectionTypes.intersection_of_tables
IntersectionTypes.intersection_of_tables_with_never_properties
IntersectionTypes.intersection_of_tables_with_top_properties IntersectionTypes.intersection_of_tables_with_top_properties
IntersectionTypes.less_greedy_unification_with_intersection_types IntersectionTypes.less_greedy_unification_with_intersection_types
IntersectionTypes.overloaded_functions_mentioning_generic
IntersectionTypes.overloaded_functions_mentioning_generic_packs
IntersectionTypes.overloaded_functions_mentioning_generics
IntersectionTypes.overloaded_functions_returning_intersections
IntersectionTypes.overloadeded_functions_with_never_arguments
IntersectionTypes.overloadeded_functions_with_never_result
IntersectionTypes.overloadeded_functions_with_overlapping_results_and_variadics
IntersectionTypes.overloadeded_functions_with_unknown_arguments
IntersectionTypes.overloadeded_functions_with_unknown_result
IntersectionTypes.overloadeded_functions_with_weird_typepacks_1
IntersectionTypes.overloadeded_functions_with_weird_typepacks_2
IntersectionTypes.overloadeded_functions_with_weird_typepacks_3
IntersectionTypes.overloadeded_functions_with_weird_typepacks_4
IntersectionTypes.select_correct_union_fn IntersectionTypes.select_correct_union_fn
IntersectionTypes.should_still_pick_an_overload_whose_arguments_are_unions IntersectionTypes.should_still_pick_an_overload_whose_arguments_are_unions
IntersectionTypes.table_intersection_write_sealed_indirect IntersectionTypes.table_intersection_write_sealed_indirect
IntersectionTypes.table_write_sealed_indirect IntersectionTypes.table_write_sealed_indirect
IntersectionTypes.union_saturate_overloaded_functions
Negations.cofinite_strings_can_be_compared_for_equality
Normalize.higher_order_function_with_annotation Normalize.higher_order_function_with_annotation
Normalize.negations_of_tables Normalize.negations_of_tables
Normalize.specific_functions_cannot_be_negated Normalize.specific_functions_cannot_be_negated
ProvisionalTests.assign_table_with_refined_property_with_a_similar_type_is_illegal ProvisionalTests.assign_table_with_refined_property_with_a_similar_type_is_illegal
ProvisionalTests.bail_early_if_unification_is_too_complicated
ProvisionalTests.choose_the_right_overload_for_pcall ProvisionalTests.choose_the_right_overload_for_pcall
ProvisionalTests.do_not_ice_when_trying_to_pick_first_of_generic_type_pack ProvisionalTests.do_not_ice_when_trying_to_pick_first_of_generic_type_pack
ProvisionalTests.error_on_eq_metamethod_returning_a_type_other_than_boolean ProvisionalTests.error_on_eq_metamethod_returning_a_type_other_than_boolean
ProvisionalTests.expected_type_should_be_a_helpful_deduction_guide_for_function_calls
ProvisionalTests.floating_generics_should_not_be_allowed
ProvisionalTests.free_is_not_bound_to_any ProvisionalTests.free_is_not_bound_to_any
ProvisionalTests.free_options_can_be_unified_together ProvisionalTests.free_options_can_be_unified_together
ProvisionalTests.free_options_cannot_be_unified_together ProvisionalTests.free_options_cannot_be_unified_together
ProvisionalTests.function_returns_many_things_but_first_of_it_is_forgotten ProvisionalTests.function_returns_many_things_but_first_of_it_is_forgotten
ProvisionalTests.generic_type_leak_to_module_interface ProvisionalTests.generic_type_leak_to_module_interface
ProvisionalTests.greedy_inference_with_shared_self_triggers_function_with_no_returns ProvisionalTests.greedy_inference_with_shared_self_triggers_function_with_no_returns
ProvisionalTests.it_should_be_agnostic_of_actual_size
ProvisionalTests.luau-polyfill.Array.filter
ProvisionalTests.luau-polyfill.Map.entries
ProvisionalTests.optional_class_instances_are_invariant
ProvisionalTests.pcall_returns_at_least_two_value_but_function_returns_nothing ProvisionalTests.pcall_returns_at_least_two_value_but_function_returns_nothing
ProvisionalTests.setmetatable_constrains_free_type_into_free_table ProvisionalTests.setmetatable_constrains_free_type_into_free_table
ProvisionalTests.specialization_binds_with_prototypes_too_early ProvisionalTests.specialization_binds_with_prototypes_too_early
ProvisionalTests.table_insert_with_a_singleton_argument ProvisionalTests.table_insert_with_a_singleton_argument
ProvisionalTests.table_unification_infinite_recursion ProvisionalTests.table_unification_infinite_recursion
ProvisionalTests.typeguard_inference_incomplete ProvisionalTests.typeguard_inference_incomplete
ProvisionalTests.while_body_are_also_refined
ProvisionalTests.xpcall_returns_what_f_returns ProvisionalTests.xpcall_returns_what_f_returns
RefinementTest.call_an_incompatible_function_after_using_typeguard RefinementTest.apply_refinements_on_astexprindexexpr_whose_subscript_expr_is_constant_string
RefinementTest.correctly_lookup_property_whose_base_was_previously_refined
RefinementTest.dataflow_analysis_can_tell_refinements_when_its_appropriate_to_refine_into_nil_or_never RefinementTest.dataflow_analysis_can_tell_refinements_when_its_appropriate_to_refine_into_nil_or_never
RefinementTest.discriminate_from_truthiness_of_x RefinementTest.discriminate_from_truthiness_of_x
RefinementTest.fail_to_refine_a_property_of_subscript_expression RefinementTest.fail_to_refine_a_property_of_subscript_expression
RefinementTest.falsiness_of_TruthyPredicate_narrows_into_nil RefinementTest.falsiness_of_TruthyPredicate_narrows_into_nil
RefinementTest.impossible_type_narrow_is_not_an_error
RefinementTest.index_on_a_refined_property
RefinementTest.isa_type_refinement_must_be_known_ahead_of_time RefinementTest.isa_type_refinement_must_be_known_ahead_of_time
RefinementTest.luau_polyfill_isindexkey_refine_conjunction
RefinementTest.luau_polyfill_isindexkey_refine_conjunction_variant
RefinementTest.narrow_property_of_a_bounded_variable RefinementTest.narrow_property_of_a_bounded_variable
RefinementTest.nonoptional_type_can_narrow_to_nil_if_sense_is_true RefinementTest.nonoptional_type_can_narrow_to_nil_if_sense_is_true
RefinementTest.not_t_or_some_prop_of_t RefinementTest.not_t_or_some_prop_of_t
RefinementTest.refine_a_property_of_some_global RefinementTest.refine_a_property_of_some_global
RefinementTest.refine_unknown_to_table_then_clone_it
RefinementTest.string_not_equal_to_string_or_nil
RefinementTest.truthy_constraint_on_properties RefinementTest.truthy_constraint_on_properties
RefinementTest.type_annotations_arent_relevant_when_doing_dataflow_analysis
RefinementTest.type_narrow_to_vector RefinementTest.type_narrow_to_vector
RefinementTest.typeguard_cast_free_table_to_vector RefinementTest.typeguard_cast_free_table_to_vector
RefinementTest.typeguard_in_assert_position RefinementTest.typeguard_in_assert_position
RefinementTest.x_as_any_if_x_is_instance_elseif_x_is_table RefinementTest.x_as_any_if_x_is_instance_elseif_x_is_table
TableTests.a_free_shape_can_turn_into_a_scalar_directly
TableTests.a_free_shape_can_turn_into_a_scalar_if_it_is_compatible TableTests.a_free_shape_can_turn_into_a_scalar_if_it_is_compatible
TableTests.a_free_shape_cannot_turn_into_a_scalar_if_it_is_not_compatible TableTests.a_free_shape_cannot_turn_into_a_scalar_if_it_is_not_compatible
TableTests.accidentally_checked_prop_in_opposite_branch
TableTests.array_factory_function
TableTests.call_method TableTests.call_method
TableTests.cannot_change_type_of_unsealed_table_prop TableTests.cannot_change_type_of_unsealed_table_prop
TableTests.casting_tables_with_props_into_table_with_indexer2
TableTests.casting_tables_with_props_into_table_with_indexer3 TableTests.casting_tables_with_props_into_table_with_indexer3
TableTests.casting_tables_with_props_into_table_with_indexer4 TableTests.casting_tables_with_props_into_table_with_indexer4
TableTests.casting_unsealed_tables_with_props_into_table_with_indexer TableTests.casting_unsealed_tables_with_props_into_table_with_indexer
TableTests.certain_properties_of_table_literal_arguments_can_be_covariant
TableTests.checked_prop_too_early TableTests.checked_prop_too_early
TableTests.cli_84607_missing_prop_in_array_or_dict TableTests.cli_84607_missing_prop_in_array_or_dict
TableTests.common_table_element_general
TableTests.common_table_element_inner_index
TableTests.common_table_element_inner_prop
TableTests.common_table_element_list
TableTests.common_table_element_union_assignment
TableTests.common_table_element_union_in_call
TableTests.common_table_element_union_in_call_tail
TableTests.common_table_element_union_in_prop
TableTests.confusing_indexing
TableTests.cyclic_shifted_tables TableTests.cyclic_shifted_tables
TableTests.disallow_indexing_into_an_unsealed_table_with_no_indexer_in_strict_mode TableTests.disallow_indexing_into_an_unsealed_table_with_no_indexer_in_strict_mode
TableTests.dont_crash_when_setmetatable_does_not_produce_a_metatabletypevar TableTests.dont_crash_when_setmetatable_does_not_produce_a_metatabletypevar
@ -183,29 +278,54 @@ TableTests.dont_extend_unsealed_tables_in_rvalue_position
TableTests.dont_hang_when_trying_to_look_up_in_cyclic_metatable_index TableTests.dont_hang_when_trying_to_look_up_in_cyclic_metatable_index
TableTests.dont_leak_free_table_props TableTests.dont_leak_free_table_props
TableTests.dont_quantify_table_that_belongs_to_outer_scope TableTests.dont_quantify_table_that_belongs_to_outer_scope
TableTests.dont_seal_an_unsealed_table_by_passing_it_to_a_function_that_takes_a_sealed_table
TableTests.dont_suggest_exact_match_keys TableTests.dont_suggest_exact_match_keys
TableTests.error_detailed_indexer_key
TableTests.error_detailed_indexer_value
TableTests.error_detailed_metatable_prop TableTests.error_detailed_metatable_prop
TableTests.error_detailed_prop
TableTests.error_detailed_prop_nested
TableTests.expected_indexer_from_table_union
TableTests.expected_indexer_value_type_extra
TableTests.expected_indexer_value_type_extra_2
TableTests.explicitly_typed_table TableTests.explicitly_typed_table
TableTests.explicitly_typed_table_error
TableTests.explicitly_typed_table_with_indexer TableTests.explicitly_typed_table_with_indexer
TableTests.extend_unsealed_table_with_metatable
TableTests.generalize_table_argument TableTests.generalize_table_argument
TableTests.generic_table_instantiation_potential_regression TableTests.generic_table_instantiation_potential_regression
TableTests.give_up_after_one_metatable_index_look_up TableTests.give_up_after_one_metatable_index_look_up
TableTests.indexer_mismatch
TableTests.indexer_on_sealed_table_must_unify_with_free_table
TableTests.indexers_get_quantified_too TableTests.indexers_get_quantified_too
TableTests.inequality_operators_imply_exactly_matching_types TableTests.inequality_operators_imply_exactly_matching_types
TableTests.infer_array_2 TableTests.infer_array_2
TableTests.infer_indexer_for_left_unsealed_table_from_right_hand_table_with_indexer
TableTests.infer_indexer_from_its_function_return_type
TableTests.infer_indexer_from_value_property_in_literal
TableTests.inferred_return_type_of_free_table TableTests.inferred_return_type_of_free_table
TableTests.instantiate_table_cloning_3 TableTests.instantiate_table_cloning_3
TableTests.instantiate_tables_at_scope_level
TableTests.invariant_table_properties_means_instantiating_tables_in_assignment_is_unsound
TableTests.invariant_table_properties_means_instantiating_tables_in_call_is_unsound
TableTests.leaking_bad_metatable_errors TableTests.leaking_bad_metatable_errors
TableTests.less_exponential_blowup_please TableTests.less_exponential_blowup_please
TableTests.metatable_mismatch_should_fail
TableTests.missing_metatable_for_sealed_tables_do_not_get_inferred TableTests.missing_metatable_for_sealed_tables_do_not_get_inferred
TableTests.mixed_tables_with_implicit_numbered_keys TableTests.mixed_tables_with_implicit_numbered_keys
TableTests.ok_to_provide_a_subtype_during_construction TableTests.ok_to_provide_a_subtype_during_construction
TableTests.ok_to_set_nil_even_on_non_lvalue_base_expr
TableTests.okay_to_add_property_to_unsealed_tables_by_assignment TableTests.okay_to_add_property_to_unsealed_tables_by_assignment
TableTests.okay_to_add_property_to_unsealed_tables_by_function_call TableTests.okay_to_add_property_to_unsealed_tables_by_function_call
TableTests.only_ascribe_synthetic_names_at_module_scope
TableTests.oop_indexer_works TableTests.oop_indexer_works
TableTests.oop_polymorphic TableTests.oop_polymorphic
TableTests.open_table_unification_2
TableTests.pass_a_union_of_tables_to_a_function_that_requires_a_table TableTests.pass_a_union_of_tables_to_a_function_that_requires_a_table
TableTests.pass_a_union_of_tables_to_a_function_that_requires_a_table_2 TableTests.pass_a_union_of_tables_to_a_function_that_requires_a_table_2
TableTests.passing_compatible_unions_to_a_generic_table_without_crashing
TableTests.prop_access_on_key_whose_types_mismatches
TableTests.prop_access_on_unions_of_indexers_where_key_whose_types_mismatches
TableTests.quantify_even_that_table_was_never_exported_at_all TableTests.quantify_even_that_table_was_never_exported_at_all
TableTests.quantify_metatables_of_metatables_of_table TableTests.quantify_metatables_of_metatables_of_table
TableTests.reasonable_error_when_adding_a_nonexistent_property_to_an_array_like_table TableTests.reasonable_error_when_adding_a_nonexistent_property_to_an_array_like_table
@ -214,18 +334,25 @@ TableTests.right_table_missing_key2
TableTests.scalar_is_a_subtype_of_a_compatible_polymorphic_shape_type TableTests.scalar_is_a_subtype_of_a_compatible_polymorphic_shape_type
TableTests.scalar_is_not_a_subtype_of_a_compatible_polymorphic_shape_type TableTests.scalar_is_not_a_subtype_of_a_compatible_polymorphic_shape_type
TableTests.sealed_table_indexers_must_unify TableTests.sealed_table_indexers_must_unify
TableTests.sealed_table_value_can_infer_an_indexer
TableTests.shared_selfs TableTests.shared_selfs
TableTests.shared_selfs_from_free_param TableTests.shared_selfs_from_free_param
TableTests.shared_selfs_through_metatables TableTests.shared_selfs_through_metatables
TableTests.subproperties_can_also_be_covariantly_tested
TableTests.table_call_metamethod_basic TableTests.table_call_metamethod_basic
TableTests.table_call_metamethod_generic TableTests.table_call_metamethod_generic
TableTests.table_function_check_use_after_free
TableTests.table_insert_should_cope_with_optional_properties_in_strict
TableTests.table_param_width_subtyping_1 TableTests.table_param_width_subtyping_1
TableTests.table_param_width_subtyping_2 TableTests.table_param_width_subtyping_2
TableTests.table_param_width_subtyping_3
TableTests.table_simple_call TableTests.table_simple_call
TableTests.table_subtyping_with_extra_props_dont_report_multiple_errors TableTests.table_subtyping_with_extra_props_dont_report_multiple_errors
TableTests.table_subtyping_with_missing_props_dont_report_multiple_errors TableTests.table_subtyping_with_missing_props_dont_report_multiple_errors
TableTests.table_subtyping_with_missing_props_dont_report_multiple_errors2 TableTests.table_subtyping_with_missing_props_dont_report_multiple_errors2
TableTests.table_unification_4
TableTests.table_unifies_into_map TableTests.table_unifies_into_map
TableTests.top_table_type
TableTests.type_mismatch_on_massive_table_is_cut_short TableTests.type_mismatch_on_massive_table_is_cut_short
TableTests.unifying_tables_shouldnt_uaf1 TableTests.unifying_tables_shouldnt_uaf1
TableTests.used_colon_instead_of_dot TableTests.used_colon_instead_of_dot
@ -236,31 +363,41 @@ ToString.exhaustive_toString_of_cyclic_table
ToString.free_types ToString.free_types
ToString.named_metatable_toStringNamedFunction ToString.named_metatable_toStringNamedFunction
ToString.pick_distinct_names_for_mixed_explicit_and_implicit_generics ToString.pick_distinct_names_for_mixed_explicit_and_implicit_generics
ToString.tostring_error_mismatch
ToString.toStringDetailed2 ToString.toStringDetailed2
ToString.toStringErrorPack ToString.toStringErrorPack
ToString.toStringNamedFunction_generic_pack ToString.toStringNamedFunction_generic_pack
TryUnifyTests.members_of_failed_typepack_unification_are_unified_with_errorType TryUnifyTests.members_of_failed_typepack_unification_are_unified_with_errorType
TryUnifyTests.result_of_failed_typepack_unification_is_constrained TryUnifyTests.result_of_failed_typepack_unification_is_constrained
TryUnifyTests.typepack_unification_should_trim_free_tails TryUnifyTests.typepack_unification_should_trim_free_tails
TryUnifyTests.uninhabited_table_sub_anything
TryUnifyTests.uninhabited_table_sub_never
TryUnifyTests.variadics_should_use_reversed_properly TryUnifyTests.variadics_should_use_reversed_properly
TypeAliases.alias_expands_to_bare_reference_to_imported_type
TypeAliases.dont_lose_track_of_PendingExpansionTypes_after_substitution TypeAliases.dont_lose_track_of_PendingExpansionTypes_after_substitution
TypeAliases.free_variables_from_typeof_in_aliases
TypeAliases.generic_param_remap TypeAliases.generic_param_remap
TypeAliases.mismatched_generic_type_param TypeAliases.mismatched_generic_type_param
TypeAliases.mutually_recursive_aliases
TypeAliases.mutually_recursive_generic_aliases
TypeAliases.mutually_recursive_types_restriction_not_ok_1 TypeAliases.mutually_recursive_types_restriction_not_ok_1
TypeAliases.mutually_recursive_types_restriction_not_ok_2 TypeAliases.mutually_recursive_types_restriction_not_ok_2
TypeAliases.mutually_recursive_types_swapsies_not_ok TypeAliases.mutually_recursive_types_swapsies_not_ok
TypeAliases.recursive_types_restriction_not_ok TypeAliases.recursive_types_restriction_not_ok
TypeAliases.report_shadowed_aliases TypeAliases.report_shadowed_aliases
TypeAliases.stringify_optional_parameterized_alias
TypeAliases.type_alias_local_mutation TypeAliases.type_alias_local_mutation
TypeAliases.type_alias_local_rename TypeAliases.type_alias_local_rename
TypeAliases.type_alias_locations TypeAliases.type_alias_locations
TypeAliases.type_alias_of_an_imported_recursive_generic_type TypeAliases.type_alias_of_an_imported_recursive_generic_type
TypeAliases.use_table_name_and_generic_params_in_errors
TypeFamilyTests.add_family_at_work TypeFamilyTests.add_family_at_work
TypeFamilyTests.family_as_fn_arg TypeFamilyTests.family_as_fn_arg
TypeFamilyTests.family_as_fn_ret TypeFamilyTests.family_as_fn_ret
TypeFamilyTests.function_internal_families TypeFamilyTests.function_internal_families
TypeFamilyTests.internal_families_raise_errors TypeFamilyTests.internal_families_raise_errors
TypeFamilyTests.table_internal_families TypeFamilyTests.table_internal_families
TypeFamilyTests.type_families_inhabited_with_normalization
TypeFamilyTests.unsolvable_family TypeFamilyTests.unsolvable_family
TypeInfer.bidirectional_checking_of_callback_property TypeInfer.bidirectional_checking_of_callback_property
TypeInfer.check_expr_recursion_limit TypeInfer.check_expr_recursion_limit
@ -269,13 +406,22 @@ TypeInfer.cli_39932_use_unifier_in_ensure_methods
TypeInfer.cli_50041_committing_txnlog_in_apollo_client_error TypeInfer.cli_50041_committing_txnlog_in_apollo_client_error
TypeInfer.dont_report_type_errors_within_an_AstExprError TypeInfer.dont_report_type_errors_within_an_AstExprError
TypeInfer.dont_report_type_errors_within_an_AstStatError TypeInfer.dont_report_type_errors_within_an_AstStatError
TypeInfer.follow_on_new_types_in_substitution
TypeInfer.fuzz_free_table_type_change_during_index_check TypeInfer.fuzz_free_table_type_change_during_index_check
TypeInfer.infer_assignment_value_types_mutable_lval TypeInfer.globals_are_banned_in_strict_mode
TypeInfer.if_statement
TypeInfer.infer_locals_via_assignment_from_its_call_site
TypeInfer.infer_locals_with_nil_value
TypeInfer.infer_through_group_expr
TypeInfer.infer_type_assertion_value_type
TypeInfer.interesting_local_type_inference_case
TypeInfer.no_stack_overflow_from_isoptional TypeInfer.no_stack_overflow_from_isoptional
TypeInfer.promote_tail_type_packs TypeInfer.promote_tail_type_packs
TypeInfer.recursive_function_that_invokes_itself_with_a_refinement_of_its_parameter TypeInfer.recursive_function_that_invokes_itself_with_a_refinement_of_its_parameter
TypeInfer.recursive_function_that_invokes_itself_with_a_refinement_of_its_parameter_2 TypeInfer.recursive_function_that_invokes_itself_with_a_refinement_of_its_parameter_2
TypeInfer.tc_after_error_recovery_no_replacement_name_in_error TypeInfer.tc_after_error_recovery_no_replacement_name_in_error
TypeInfer.tc_error
TypeInfer.tc_error_2
TypeInfer.tc_if_else_expressions_expected_type_3 TypeInfer.tc_if_else_expressions_expected_type_3
TypeInfer.type_infer_recursion_limit_no_ice TypeInfer.type_infer_recursion_limit_no_ice
TypeInfer.type_infer_recursion_limit_normalizer TypeInfer.type_infer_recursion_limit_normalizer
@ -288,22 +434,36 @@ TypeInferAnyError.for_in_loop_iterator_returns_any
TypeInferAnyError.for_in_loop_iterator_returns_any2 TypeInferAnyError.for_in_loop_iterator_returns_any2
TypeInferAnyError.intersection_of_any_can_have_props TypeInferAnyError.intersection_of_any_can_have_props
TypeInferAnyError.replace_every_free_type_when_unifying_a_complex_function_with_any TypeInferAnyError.replace_every_free_type_when_unifying_a_complex_function_with_any
TypeInferAnyError.union_of_types_regression_test TypeInferAnyError.type_error_addition
TypeInferClasses.callable_classes TypeInferClasses.callable_classes
TypeInferClasses.can_read_prop_of_base_class_using_string
TypeInferClasses.cannot_unify_class_instance_with_primitive
TypeInferClasses.class_type_mismatch_with_name_conflict TypeInferClasses.class_type_mismatch_with_name_conflict
TypeInferClasses.detailed_class_unification_error TypeInferClasses.detailed_class_unification_error
TypeInferClasses.index_instance_property TypeInferClasses.index_instance_property
TypeInferClasses.indexable_classes
TypeInferClasses.table_class_unification_reports_sane_errors_for_missing_properties TypeInferClasses.table_class_unification_reports_sane_errors_for_missing_properties
TypeInferClasses.table_indexers_are_invariant
TypeInferClasses.type_mismatch_invariance_required_for_error
TypeInferClasses.we_can_report_when_someone_is_trying_to_use_a_table_rather_than_a_class
TypeInferFunctions.apply_of_lambda_with_inferred_and_explicit_types TypeInferFunctions.apply_of_lambda_with_inferred_and_explicit_types
TypeInferFunctions.cannot_hoist_interior_defns_into_signature TypeInferFunctions.cannot_hoist_interior_defns_into_signature
TypeInferFunctions.check_function_bodies
TypeInferFunctions.concrete_functions_are_not_supertypes_of_function
TypeInferFunctions.dont_assert_when_the_tarjan_limit_is_exceeded_during_generalization TypeInferFunctions.dont_assert_when_the_tarjan_limit_is_exceeded_during_generalization
TypeInferFunctions.dont_give_other_overloads_message_if_only_one_argument_matching_overload_exists TypeInferFunctions.dont_give_other_overloads_message_if_only_one_argument_matching_overload_exists
TypeInferFunctions.dont_infer_parameter_types_for_functions_from_their_call_site TypeInferFunctions.dont_infer_parameter_types_for_functions_from_their_call_site
TypeInferFunctions.error_detailed_function_mismatch_arg
TypeInferFunctions.error_detailed_function_mismatch_arg_count
TypeInferFunctions.error_detailed_function_mismatch_ret
TypeInferFunctions.error_detailed_function_mismatch_ret_count
TypeInferFunctions.error_detailed_function_mismatch_ret_mult
TypeInferFunctions.function_cast_error_uses_correct_language TypeInferFunctions.function_cast_error_uses_correct_language
TypeInferFunctions.function_decl_non_self_sealed_overwrite_2 TypeInferFunctions.function_decl_non_self_sealed_overwrite_2
TypeInferFunctions.function_decl_non_self_unsealed_overwrite TypeInferFunctions.function_decl_non_self_unsealed_overwrite
TypeInferFunctions.function_does_not_return_enough_values TypeInferFunctions.function_does_not_return_enough_values
TypeInferFunctions.function_exprs_are_generalized_at_signature_scope_not_enclosing TypeInferFunctions.function_exprs_are_generalized_at_signature_scope_not_enclosing
TypeInferFunctions.function_is_supertype_of_concrete_functions
TypeInferFunctions.function_statement_sealed_table_assignment_through_indexer TypeInferFunctions.function_statement_sealed_table_assignment_through_indexer
TypeInferFunctions.generic_packs_are_not_variadic TypeInferFunctions.generic_packs_are_not_variadic
TypeInferFunctions.higher_order_function_2 TypeInferFunctions.higher_order_function_2
@ -311,14 +471,22 @@ TypeInferFunctions.higher_order_function_3
TypeInferFunctions.higher_order_function_4 TypeInferFunctions.higher_order_function_4
TypeInferFunctions.improved_function_arg_mismatch_errors TypeInferFunctions.improved_function_arg_mismatch_errors
TypeInferFunctions.infer_anonymous_function_arguments TypeInferFunctions.infer_anonymous_function_arguments
TypeInferFunctions.infer_anonymous_function_arguments_outside_call
TypeInferFunctions.infer_generic_function_function_argument TypeInferFunctions.infer_generic_function_function_argument
TypeInferFunctions.infer_generic_function_function_argument_overloaded TypeInferFunctions.infer_generic_function_function_argument_overloaded
TypeInferFunctions.infer_generic_lib_function_function_argument TypeInferFunctions.infer_generic_lib_function_function_argument
TypeInferFunctions.infer_return_type_from_selected_overload TypeInferFunctions.infer_return_type_from_selected_overload
TypeInferFunctions.infer_return_value_type
TypeInferFunctions.infer_that_function_does_not_return_a_table TypeInferFunctions.infer_that_function_does_not_return_a_table
TypeInferFunctions.inferred_higher_order_functions_are_quantified_at_the_right_time3
TypeInferFunctions.instantiated_type_packs_must_have_a_non_null_scope
TypeInferFunctions.it_is_ok_to_oversaturate_a_higher_order_function_argument TypeInferFunctions.it_is_ok_to_oversaturate_a_higher_order_function_argument
TypeInferFunctions.luau_subtyping_is_np_hard TypeInferFunctions.luau_subtyping_is_np_hard
TypeInferFunctions.no_lossy_function_type TypeInferFunctions.no_lossy_function_type
TypeInferFunctions.num_is_solved_after_num_or_str
TypeInferFunctions.num_is_solved_before_num_or_str
TypeInferFunctions.occurs_check_failure_in_function_return_type
TypeInferFunctions.other_things_are_not_related_to_function
TypeInferFunctions.param_1_and_2_both_takes_the_same_generic_but_their_arguments_are_incompatible TypeInferFunctions.param_1_and_2_both_takes_the_same_generic_but_their_arguments_are_incompatible
TypeInferFunctions.param_1_and_2_both_takes_the_same_generic_but_their_arguments_are_incompatible_2 TypeInferFunctions.param_1_and_2_both_takes_the_same_generic_but_their_arguments_are_incompatible_2
TypeInferFunctions.record_matching_overload TypeInferFunctions.record_matching_overload
@ -330,7 +498,9 @@ TypeInferFunctions.too_few_arguments_variadic_generic2
TypeInferFunctions.too_many_arguments_error_location TypeInferFunctions.too_many_arguments_error_location
TypeInferFunctions.too_many_return_values_in_parentheses TypeInferFunctions.too_many_return_values_in_parentheses
TypeInferFunctions.too_many_return_values_no_function TypeInferFunctions.too_many_return_values_no_function
TypeInferFunctions.vararg_function_is_quantified
TypeInferLoops.cli_68448_iterators_need_not_accept_nil TypeInferLoops.cli_68448_iterators_need_not_accept_nil
TypeInferLoops.dcr_iteration_explore_raycast_minimization
TypeInferLoops.dcr_iteration_on_never_gives_never TypeInferLoops.dcr_iteration_on_never_gives_never
TypeInferLoops.dcr_xpath_candidates TypeInferLoops.dcr_xpath_candidates
TypeInferLoops.for_in_loop TypeInferLoops.for_in_loop
@ -341,8 +511,12 @@ TypeInferLoops.for_in_loop_on_non_function
TypeInferLoops.for_in_loop_with_custom_iterator TypeInferLoops.for_in_loop_with_custom_iterator
TypeInferLoops.for_in_loop_with_incompatible_args_to_iterator TypeInferLoops.for_in_loop_with_incompatible_args_to_iterator
TypeInferLoops.for_in_loop_with_next TypeInferLoops.for_in_loop_with_next
TypeInferLoops.for_in_with_generic_next
TypeInferLoops.for_in_with_just_one_iterator_is_ok TypeInferLoops.for_in_with_just_one_iterator_is_ok
TypeInferLoops.for_loop
TypeInferLoops.ipairs_produces_integral_indices TypeInferLoops.ipairs_produces_integral_indices
TypeInferLoops.iteration_no_table_passed
TypeInferLoops.iteration_regression_issue_69967
TypeInferLoops.iteration_regression_issue_69967_alt TypeInferLoops.iteration_regression_issue_69967_alt
TypeInferLoops.loop_iter_basic TypeInferLoops.loop_iter_basic
TypeInferLoops.loop_iter_metamethod_nil TypeInferLoops.loop_iter_metamethod_nil
@ -351,11 +525,14 @@ TypeInferLoops.loop_iter_metamethod_ok_with_inference
TypeInferLoops.loop_iter_trailing_nil TypeInferLoops.loop_iter_trailing_nil
TypeInferLoops.loop_typecheck_crash_on_empty_optional TypeInferLoops.loop_typecheck_crash_on_empty_optional
TypeInferLoops.properly_infer_iteratee_is_a_free_table TypeInferLoops.properly_infer_iteratee_is_a_free_table
TypeInferLoops.repeat_loop
TypeInferLoops.unreachable_code_after_infinite_loop TypeInferLoops.unreachable_code_after_infinite_loop
TypeInferLoops.varlist_declared_by_for_in_loop_should_be_free TypeInferLoops.varlist_declared_by_for_in_loop_should_be_free
TypeInferLoops.while_loop
TypeInferModules.bound_free_table_export_is_ok TypeInferModules.bound_free_table_export_is_ok
TypeInferModules.do_not_modify_imported_types_4 TypeInferModules.do_not_modify_imported_types_4
TypeInferModules.do_not_modify_imported_types_5 TypeInferModules.do_not_modify_imported_types_5
TypeInferModules.general_require_call_expression
TypeInferModules.module_type_conflict TypeInferModules.module_type_conflict
TypeInferModules.module_type_conflict_instantiated TypeInferModules.module_type_conflict_instantiated
TypeInferModules.require_failed_module TypeInferModules.require_failed_module
@ -371,13 +548,20 @@ TypeInferOOP.table_oop
TypeInferOperators.add_type_family_works TypeInferOperators.add_type_family_works
TypeInferOperators.and_binexps_dont_unify TypeInferOperators.and_binexps_dont_unify
TypeInferOperators.cli_38355_recursive_union TypeInferOperators.cli_38355_recursive_union
TypeInferOperators.compound_assign_mismatch_metatable
TypeInferOperators.compound_assign_mismatch_op
TypeInferOperators.compound_assign_mismatch_result
TypeInferOperators.compound_assign_result_must_be_compatible_with_var TypeInferOperators.compound_assign_result_must_be_compatible_with_var
TypeInferOperators.concat_op_on_free_lhs_and_string_rhs TypeInferOperators.concat_op_on_free_lhs_and_string_rhs
TypeInferOperators.concat_op_on_string_lhs_and_free_rhs TypeInferOperators.concat_op_on_string_lhs_and_free_rhs
TypeInferOperators.disallow_string_and_types_without_metatables_from_arithmetic_binary_ops TypeInferOperators.disallow_string_and_types_without_metatables_from_arithmetic_binary_ops
TypeInferOperators.luau-polyfill.Array.startswith
TypeInferOperators.luau_polyfill_is_array TypeInferOperators.luau_polyfill_is_array
TypeInferOperators.normalize_strings_comparison
TypeInferOperators.operator_eq_completely_incompatible TypeInferOperators.operator_eq_completely_incompatible
TypeInferOperators.operator_eq_verifies_types_do_intersect
TypeInferOperators.reducing_and TypeInferOperators.reducing_and
TypeInferOperators.refine_and_or
TypeInferOperators.strict_binary_op_where_lhs_unknown TypeInferOperators.strict_binary_op_where_lhs_unknown
TypeInferOperators.typecheck_overloaded_multiply_that_is_an_intersection TypeInferOperators.typecheck_overloaded_multiply_that_is_an_intersection
TypeInferOperators.typecheck_overloaded_multiply_that_is_an_intersection_on_rhs TypeInferOperators.typecheck_overloaded_multiply_that_is_an_intersection_on_rhs
@ -388,22 +572,51 @@ TypeInferOperators.unrelated_classes_cannot_be_compared
TypeInferOperators.unrelated_primitives_cannot_be_compared TypeInferOperators.unrelated_primitives_cannot_be_compared
TypeInferPrimitives.CheckMethodsOfNumber TypeInferPrimitives.CheckMethodsOfNumber
TypeInferPrimitives.string_index TypeInferPrimitives.string_index
TypeInferUnknownNever.array_like_table_of_never_is_inhabitable
TypeInferUnknownNever.index_on_union_of_tables_for_properties_that_is_never
TypeInferUnknownNever.index_on_union_of_tables_for_properties_that_is_sorta_never
TypeInferUnknownNever.length_of_never TypeInferUnknownNever.length_of_never
TypeInferUnknownNever.math_operators_and_never TypeInferUnknownNever.math_operators_and_never
TypePackTests.fuzz_typepack_iter_follow_2 TypePackTests.fuzz_typepack_iter_follow_2
TypePackTests.pack_tail_unification_check TypePackTests.pack_tail_unification_check
TypePackTests.parenthesized_varargs_returns_any
TypePackTests.type_alias_backwards_compatible TypePackTests.type_alias_backwards_compatible
TypePackTests.type_alias_default_type_errors TypePackTests.type_alias_default_type_errors
TypePackTests.type_packs_with_tails_in_vararg_adjustment TypePackTests.type_packs_with_tails_in_vararg_adjustment
TypePackTests.unify_variadic_tails_in_arguments
TypePackTests.unify_variadic_tails_in_arguments_free
TypePackTests.variadic_argument_tail
TypeSingletons.enums_using_singletons_mismatch
TypeSingletons.error_detailed_tagged_union_mismatch_bool
TypeSingletons.error_detailed_tagged_union_mismatch_string
TypeSingletons.function_args_infer_singletons TypeSingletons.function_args_infer_singletons
TypeSingletons.function_call_with_singletons TypeSingletons.function_call_with_singletons
TypeSingletons.function_call_with_singletons_mismatch TypeSingletons.function_call_with_singletons_mismatch
TypeSingletons.overloaded_function_call_with_singletons TypeSingletons.overloaded_function_call_with_singletons
TypeSingletons.return_type_of_f_is_not_widened TypeSingletons.return_type_of_f_is_not_widened
TypeSingletons.table_properties_singleton_strings
TypeSingletons.table_properties_type_error_escapes TypeSingletons.table_properties_type_error_escapes
TypeSingletons.widen_the_supertype_if_it_is_free_and_subtype_has_singleton TypeSingletons.widen_the_supertype_if_it_is_free_and_subtype_has_singleton
TypeSingletons.widening_happens_almost_everywhere TypeSingletons.widening_happens_almost_everywhere
UnionTypes.disallow_less_specific_assign
UnionTypes.disallow_less_specific_assign2
UnionTypes.error_detailed_optional
UnionTypes.error_detailed_union_all
UnionTypes.error_detailed_union_part
UnionTypes.generic_function_with_optional_arg
UnionTypes.index_on_a_union_type_with_missing_property UnionTypes.index_on_a_union_type_with_missing_property
UnionTypes.less_greedy_unification_with_union_types UnionTypes.less_greedy_unification_with_union_types
UnionTypes.optional_arguments_table2
UnionTypes.optional_index_error
UnionTypes.optional_length_error
UnionTypes.table_union_write_indirect UnionTypes.table_union_write_indirect
UnionTypes.unify_unsealed_table_union_check UnionTypes.unify_sealed_table_union_check
UnionTypes.union_of_functions
UnionTypes.union_of_functions_mentioning_generic_typepacks
UnionTypes.union_of_functions_mentioning_generics
UnionTypes.union_of_functions_with_mismatching_arg_arities
UnionTypes.union_of_functions_with_mismatching_arg_variadics
UnionTypes.union_of_functions_with_mismatching_result_arities
UnionTypes.union_of_functions_with_mismatching_result_variadics
UnionTypes.union_of_functions_with_variadics
UnionTypes.union_true_and_false