Sync to upstream/release/592 (#1018)

* AST queries at position where function name is will now return
AstExprLocal
* Lexer performance has been slightly improved
* Fixed incorrect string singleton autocomplete suggestions (fixes #858)
* Improved parsing error messages
* Fixed crash on null pointer access in unification (fixes #1017)
* Native code support is enabled by default and `native=1`
(make)/`LUAU_NATIVE` (CMake)/`-DLUA_CUSTOM_EXECUTION` configuration is
no longer required

New typechecker:
* New subtyping check can now handle generic functions and tables
(including those that contain cycles)

Native code generation:
* Loops with non-numeric parameters are now handled by VM to streamline
native code
* Array size check can be optimized away in SETLIST
* On failure, CodeGen::compile returns a reason
* Fixed clobbering of non-volatile xmm registers on Windows
This commit is contained in:
vegorov-rbx 2023-08-25 10:23:55 -07:00 committed by GitHub
parent c3fc0d7bc8
commit ce9414cb98
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
59 changed files with 1688 additions and 471 deletions

View File

@ -2,6 +2,7 @@
#pragma once #pragma once
#include "Luau/Type.h" #include "Luau/Type.h"
#include "Luau/UnifierSharedState.h"
#include <vector> #include <vector>
#include <optional> #include <optional>
@ -11,11 +12,12 @@ namespace Luau
template<typename A, typename B> template<typename A, typename B>
struct TryPair; struct TryPair;
struct InternalErrorReporter;
class Normalizer; class Normalizer;
struct NormalizedType; struct NormalizedType;
struct SubtypingGraph struct SubtypingResult
{ {
// Did the test succeed? // Did the test succeed?
bool isSubtype = false; bool isSubtype = false;
@ -25,39 +27,78 @@ struct SubtypingGraph
// If so, what constraints are implied by this relation? // If so, what constraints are implied by this relation?
// If not, what happened? // If not, what happened?
SubtypingGraph and_(const SubtypingGraph& other); void andAlso(const SubtypingResult& other);
SubtypingGraph or_(const SubtypingGraph& other); void orElse(const SubtypingResult& other);
static SubtypingGraph and_(const std::vector<SubtypingGraph>& results); static SubtypingResult all(const std::vector<SubtypingResult>& results);
static SubtypingGraph or_(const std::vector<SubtypingGraph>& results); static SubtypingResult any(const std::vector<SubtypingResult>& results);
}; };
struct Subtyping struct Subtyping
{ {
NotNull<BuiltinTypes> builtinTypes; NotNull<BuiltinTypes> builtinTypes;
NotNull<TypeArena> arena;
NotNull<Normalizer> normalizer; NotNull<Normalizer> normalizer;
NotNull<InternalErrorReporter> iceReporter;
enum class Variance
{
Covariant,
Contravariant
};
Variance variance = Variance::Covariant;
struct GenericBounds
{
DenseHashSet<TypeId> lowerBound{nullptr};
DenseHashSet<TypeId> upperBound{nullptr};
};
/*
* When we encounter a generic over the course of a subtyping test, we need
* to tentatively map that generic onto a type on the other side.
*/
DenseHashMap<TypeId, GenericBounds> mappedGenerics{nullptr};
DenseHashMap<TypePackId, TypePackId> mappedGenericPacks{nullptr};
using SeenSet = std::unordered_set<std::pair<TypeId, TypeId>, TypeIdPairHash>;
SeenSet seenTypes;
// TODO cache // TODO cache
// TODO cyclic types // TODO cyclic types
// TODO recursion limits // TODO recursion limits
SubtypingGraph isSubtype(TypeId subTy, TypeId superTy); SubtypingResult isSubtype(TypeId subTy, TypeId superTy);
SubtypingGraph isSubtype(TypePackId subTy, TypePackId superTy); SubtypingResult isSubtype(TypePackId subTy, TypePackId superTy);
private: private:
SubtypingResult isSubtype_(TypeId subTy, TypeId superTy);
SubtypingResult isSubtype_(TypePackId subTy, TypePackId superTy);
template<typename SubTy, typename SuperTy> template<typename SubTy, typename SuperTy>
SubtypingGraph isSubtype(const TryPair<const SubTy*, const SuperTy*>& pair); SubtypingResult isSubtype_(const TryPair<const SubTy*, const SuperTy*>& pair);
SubtypingGraph isSubtype(TypeId subTy, const UnionType* superUnion); SubtypingResult isSubtype_(TypeId subTy, const UnionType* superUnion);
SubtypingGraph isSubtype(const UnionType* subUnion, TypeId superTy); SubtypingResult isSubtype_(const UnionType* subUnion, TypeId superTy);
SubtypingGraph isSubtype(TypeId subTy, const IntersectionType* superIntersection); SubtypingResult isSubtype_(TypeId subTy, const IntersectionType* superIntersection);
SubtypingGraph isSubtype(const IntersectionType* subIntersection, TypeId superTy); SubtypingResult isSubtype_(const IntersectionType* subIntersection, TypeId superTy);
SubtypingGraph isSubtype(const PrimitiveType* subPrim, const PrimitiveType* superPrim); SubtypingResult isSubtype_(const PrimitiveType* subPrim, const PrimitiveType* superPrim);
SubtypingGraph isSubtype(const SingletonType* subSingleton, const PrimitiveType* superPrim); SubtypingResult isSubtype_(const SingletonType* subSingleton, const PrimitiveType* superPrim);
SubtypingGraph isSubtype(const SingletonType* subSingleton, const SingletonType* superSingleton); SubtypingResult isSubtype_(const SingletonType* subSingleton, const SingletonType* superSingleton);
SubtypingGraph isSubtype(const FunctionType* subFunction, const FunctionType* superFunction); SubtypingResult isSubtype_(const TableType* subTable, const TableType* superTable);
SubtypingResult isSubtype_(const FunctionType* subFunction, const FunctionType* superFunction);
SubtypingResult isSubtype_(const NormalizedType* subNorm, const NormalizedType* superNorm);
SubtypingGraph isSubtype(const NormalizedType* subNorm, const NormalizedType* superNorm); bool bindGeneric(TypeId subTp, TypeId superTp);
bool bindGeneric(TypePackId subTp, TypePackId superTp);
template <typename T, typename Container>
TypeId makeAggregateType(const Container& container, TypeId orElse);
[[noreturn]]
void unexpected(TypePackId tp);
}; };
} // namespace Luau } // namespace Luau

View File

@ -108,7 +108,7 @@ struct TryPair {
A first; A first;
B second; B second;
operator bool() const explicit operator bool() const
{ {
return bool(first) && bool(second); return bool(first) && bool(second);
} }

View File

@ -12,6 +12,7 @@
#include <algorithm> #include <algorithm>
LUAU_FASTFLAG(DebugLuauReadWriteProperties) LUAU_FASTFLAG(DebugLuauReadWriteProperties)
LUAU_FASTFLAGVARIABLE(FixFindBindingAtFunctionName, false);
namespace Luau namespace Luau
{ {
@ -148,6 +149,23 @@ struct FindNode : public AstVisitor
return false; return false;
} }
bool visit(AstStatFunction* node) override
{
if (FFlag::FixFindBindingAtFunctionName)
{
visit(static_cast<AstNode*>(node));
if (node->name->location.contains(pos))
node->name->visit(this);
else if (node->func->location.contains(pos))
node->func->visit(this);
return false;
}
else
{
return AstVisitor::visit(node);
}
}
bool visit(AstStatBlock* block) override bool visit(AstStatBlock* block) override
{ {
visit(static_cast<AstNode*>(block)); visit(static_cast<AstNode*>(block));
@ -188,6 +206,23 @@ struct FindFullAncestry final : public AstVisitor
return false; return false;
} }
bool visit(AstStatFunction* node) override
{
if (FFlag::FixFindBindingAtFunctionName)
{
visit(static_cast<AstNode*>(node));
if (node->name->location.contains(pos))
node->name->visit(this);
else if (node->func->location.contains(pos))
node->func->visit(this);
return false;
}
else
{
return AstVisitor::visit(node);
}
}
bool visit(AstNode* node) override bool visit(AstNode* node) override
{ {
if (node->location.contains(pos)) if (node->location.contains(pos))

View File

@ -15,7 +15,7 @@
LUAU_FASTFLAG(DebugLuauReadWriteProperties) LUAU_FASTFLAG(DebugLuauReadWriteProperties)
LUAU_FASTFLAGVARIABLE(LuauAnonymousAutofilled1, false); LUAU_FASTFLAGVARIABLE(LuauAnonymousAutofilled1, false);
LUAU_FASTFLAGVARIABLE(LuauAutocompleteLastTypecheck, false) LUAU_FASTFLAGVARIABLE(LuauAutocompleteLastTypecheck, false)
LUAU_FASTFLAGVARIABLE(LuauAutocompleteHideSelfArg, false) LUAU_FASTFLAGVARIABLE(LuauAutocompleteStringLiteralBounds, false);
static const std::unordered_set<std::string> kStatementStartingKeywords = { static const std::unordered_set<std::string> kStatementStartingKeywords = {
"while", "if", "local", "repeat", "function", "do", "for", "return", "break", "continue", "type", "export"}; "while", "if", "local", "repeat", "function", "do", "for", "return", "break", "continue", "type", "export"};
@ -283,8 +283,6 @@ static void autocompleteProps(const Module& module, TypeArena* typeArena, NotNul
ParenthesesRecommendation parens = ParenthesesRecommendation parens =
indexType == PropIndexType::Key ? ParenthesesRecommendation::None : getParenRecommendation(type, nodes, typeCorrect); indexType == PropIndexType::Key ? ParenthesesRecommendation::None : getParenRecommendation(type, nodes, typeCorrect);
if (FFlag::LuauAutocompleteHideSelfArg)
{
result[name] = AutocompleteEntry{ result[name] = AutocompleteEntry{
AutocompleteEntryKind::Property, AutocompleteEntryKind::Property,
type, type,
@ -300,22 +298,6 @@ static void autocompleteProps(const Module& module, TypeArena* typeArena, NotNul
indexType == PropIndexType::Colon indexType == PropIndexType::Colon
}; };
} }
else
{
result[name] = AutocompleteEntry{
AutocompleteEntryKind::Property,
type,
prop.deprecated,
isWrongIndexer(type),
typeCorrect,
containingClass,
&prop,
prop.documentationSymbol,
{},
parens
};
}
}
} }
}; };
@ -484,8 +466,19 @@ AutocompleteEntryMap autocompleteModuleTypes(const Module& module, Position posi
return result; return result;
} }
static void autocompleteStringSingleton(TypeId ty, bool addQuotes, AutocompleteEntryMap& result) static void autocompleteStringSingleton(TypeId ty, bool addQuotes, AstNode* node, Position position, AutocompleteEntryMap& result)
{ {
if (FFlag::LuauAutocompleteStringLiteralBounds)
{
if (position == node->location.begin || position == node->location.end)
{
if (auto str = node->as<AstExprConstantString>(); str && str->quoteStyle == AstExprConstantString::Quoted)
return;
else if (node->is<AstExprInterpString>())
return;
}
}
auto formatKey = [addQuotes](const std::string& key) { auto formatKey = [addQuotes](const std::string& key) {
if (addQuotes) if (addQuotes)
return "\"" + escape(key) + "\""; return "\"" + escape(key) + "\"";
@ -1238,7 +1231,7 @@ static AutocompleteContext autocompleteExpression(const SourceModule& sourceModu
result["function"] = {AutocompleteEntryKind::Keyword, std::nullopt, false, false, correctForFunction}; result["function"] = {AutocompleteEntryKind::Keyword, std::nullopt, false, false, correctForFunction};
if (auto ty = findExpectedTypeAt(module, node, position)) if (auto ty = findExpectedTypeAt(module, node, position))
autocompleteStringSingleton(*ty, true, result); autocompleteStringSingleton(*ty, true, node, position, result);
} }
return AutocompleteContext::Expression; return AutocompleteContext::Expression;
@ -1719,7 +1712,7 @@ static AutocompleteResult autocomplete(const SourceModule& sourceModule, const M
auto result = autocompleteProps(*module, typeArena, builtinTypes, *it, PropIndexType::Key, ancestry); auto result = autocompleteProps(*module, typeArena, builtinTypes, *it, PropIndexType::Key, ancestry);
if (auto nodeIt = module->astExpectedTypes.find(node->asExpr())) if (auto nodeIt = module->astExpectedTypes.find(node->asExpr()))
autocompleteStringSingleton(*nodeIt, !node->is<AstExprConstantString>(), result); autocompleteStringSingleton(*nodeIt, !node->is<AstExprConstantString>(), node, position, result);
if (!key) if (!key)
{ {
@ -1731,7 +1724,7 @@ static AutocompleteResult autocomplete(const SourceModule& sourceModule, const M
// suggest those too. // suggest those too.
if (auto ttv = get<TableType>(follow(*it)); ttv && ttv->indexer) if (auto ttv = get<TableType>(follow(*it)); ttv && ttv->indexer)
{ {
autocompleteStringSingleton(ttv->indexer->indexType, false, result); autocompleteStringSingleton(ttv->indexer->indexType, false, node, position, result);
} }
} }
@ -1768,7 +1761,7 @@ static AutocompleteResult autocomplete(const SourceModule& sourceModule, const M
AutocompleteEntryMap result; AutocompleteEntryMap result;
if (auto it = module->astExpectedTypes.find(node->asExpr())) if (auto it = module->astExpectedTypes.find(node->asExpr()))
autocompleteStringSingleton(*it, false, result); autocompleteStringSingleton(*it, false, node, position, result);
if (ancestry.size() >= 2) if (ancestry.size() >= 2)
{ {
@ -1782,7 +1775,7 @@ static AutocompleteResult autocomplete(const SourceModule& sourceModule, const M
if (binExpr->op == AstExprBinary::CompareEq || binExpr->op == AstExprBinary::CompareNe) if (binExpr->op == AstExprBinary::CompareEq || binExpr->op == AstExprBinary::CompareNe)
{ {
if (auto it = module->astTypes.find(node == binExpr->left ? binExpr->right : binExpr->left)) if (auto it = module->astTypes.find(node == binExpr->left ? binExpr->right : binExpr->left))
autocompleteStringSingleton(*it, false, result); autocompleteStringSingleton(*it, false, node, position, result);
} }
} }
} }

View File

@ -3,8 +3,12 @@
#include "Luau/Subtyping.h" #include "Luau/Subtyping.h"
#include "Luau/Common.h" #include "Luau/Common.h"
#include "Luau/Error.h"
#include "Luau/Normalize.h" #include "Luau/Normalize.h"
#include "Luau/StringUtils.h"
#include "Luau/ToString.h"
#include "Luau/Type.h" #include "Luau/Type.h"
#include "Luau/TypeArena.h"
#include "Luau/TypePack.h" #include "Luau/TypePack.h"
#include "Luau/TypeUtils.h" #include "Luau/TypeUtils.h"
@ -13,42 +17,109 @@
namespace Luau namespace Luau
{ {
SubtypingGraph SubtypingGraph::and_(const SubtypingGraph& other) struct VarianceFlipper
{ {
return SubtypingGraph{ Subtyping::Variance* variance;
isSubtype && other.isSubtype, Subtyping::Variance oldValue;
// `||` is intentional here, we want to preserve error-suppressing flag.
isErrorSuppressing || other.isErrorSuppressing, VarianceFlipper(Subtyping::Variance* v)
normalizationTooComplex || other.normalizationTooComplex, : variance(v)
}; , oldValue(*v)
{
switch (oldValue)
{
case Subtyping::Variance::Covariant:
*variance = Subtyping::Variance::Contravariant;
break;
case Subtyping::Variance::Contravariant:
*variance = Subtyping::Variance::Covariant;
break;
}
}
~VarianceFlipper()
{
*variance = oldValue;
}
};
void SubtypingResult::andAlso(const SubtypingResult& other)
{
isSubtype &= other.isSubtype;
// `|=` is intentional here, we want to preserve error related flags.
isErrorSuppressing |= other.isErrorSuppressing;
normalizationTooComplex |= other.normalizationTooComplex;
} }
SubtypingGraph SubtypingGraph::or_(const SubtypingGraph& other) void SubtypingResult::orElse(const SubtypingResult& other)
{ {
return SubtypingGraph{ isSubtype |= other.isSubtype;
isSubtype || other.isSubtype, isErrorSuppressing |= other.isErrorSuppressing;
isErrorSuppressing || other.isErrorSuppressing, normalizationTooComplex |= other.normalizationTooComplex;
normalizationTooComplex || other.normalizationTooComplex,
};
} }
SubtypingGraph SubtypingGraph::and_(const std::vector<SubtypingGraph>& results) SubtypingResult SubtypingResult::all(const std::vector<SubtypingResult>& results)
{ {
SubtypingGraph acc{true, false}; SubtypingResult acc{true, false};
for (const SubtypingGraph& current : results) for (const SubtypingResult& current : results)
acc = acc.and_(current); acc.andAlso(current);
return acc; return acc;
} }
SubtypingGraph SubtypingGraph::or_(const std::vector<SubtypingGraph>& results) SubtypingResult SubtypingResult::any(const std::vector<SubtypingResult>& results)
{ {
SubtypingGraph acc{false, false}; SubtypingResult acc{false, false};
for (const SubtypingGraph& current : results) for (const SubtypingResult& current : results)
acc = acc.or_(current); acc.orElse(current);
return acc; return acc;
} }
SubtypingGraph Subtyping::isSubtype(TypeId subTy, TypeId superTy) SubtypingResult Subtyping::isSubtype(TypeId subTy, TypeId superTy)
{
mappedGenerics.clear();
mappedGenericPacks.clear();
SubtypingResult result = isSubtype_(subTy, superTy);
for (const auto& [subTy, bounds]: mappedGenerics)
{
const auto& lb = bounds.lowerBound;
const auto& ub = bounds.upperBound;
TypeId lowerBound = makeAggregateType<UnionType>(lb, builtinTypes->neverType);
TypeId upperBound = makeAggregateType<IntersectionType>(ub, builtinTypes->unknownType);
result.andAlso(isSubtype_(lowerBound, upperBound));
}
return result;
}
SubtypingResult Subtyping::isSubtype(TypePackId subTp, TypePackId superTp)
{
return isSubtype_(subTp, superTp);
}
namespace
{
struct SeenSetPopper
{
Subtyping::SeenSet* seenTypes;
std::pair<TypeId, TypeId> pair;
SeenSetPopper(Subtyping::SeenSet* seenTypes, std::pair<TypeId, TypeId> pair)
: seenTypes(seenTypes)
, pair(pair)
{}
~SeenSetPopper()
{
seenTypes->erase(pair);
}
};
}
SubtypingResult Subtyping::isSubtype_(TypeId subTy, TypeId superTy)
{ {
subTy = follow(subTy); subTy = follow(subTy);
superTy = follow(superTy); superTy = follow(superTy);
@ -60,20 +131,25 @@ SubtypingGraph Subtyping::isSubtype(TypeId subTy, TypeId superTy)
if (subTy == superTy) if (subTy == superTy)
return {true}; return {true};
std::pair<TypeId, TypeId> typePair{subTy, superTy};
if (!seenTypes.insert(typePair).second)
return {true};
SeenSetPopper ssp{&seenTypes, typePair};
if (auto superUnion = get<UnionType>(superTy)) if (auto superUnion = get<UnionType>(superTy))
return isSubtype(subTy, superUnion); return isSubtype_(subTy, superUnion);
else if (auto subUnion = get<UnionType>(subTy)) else if (auto subUnion = get<UnionType>(subTy))
return isSubtype(subUnion, superTy); return isSubtype_(subUnion, superTy);
else if (auto superIntersection = get<IntersectionType>(superTy)) else if (auto superIntersection = get<IntersectionType>(superTy))
return isSubtype(subTy, superIntersection); return isSubtype_(subTy, superIntersection);
else if (auto subIntersection = get<IntersectionType>(subTy)) else if (auto subIntersection = get<IntersectionType>(subTy))
{ {
SubtypingGraph result = isSubtype(subIntersection, superTy); SubtypingResult result = isSubtype_(subIntersection, superTy);
if (result.isSubtype || result.isErrorSuppressing || result.normalizationTooComplex) if (result.isSubtype || result.isErrorSuppressing || result.normalizationTooComplex)
return result; return result;
else else
return isSubtype(normalizer->normalize(subTy), normalizer->normalize(superTy)); return isSubtype_(normalizer->normalize(subTy), normalizer->normalize(superTy));
} }
else if (get<AnyType>(superTy)) else if (get<AnyType>(superTy))
return {true}; // This is always true. return {true}; // This is always true.
@ -81,9 +157,11 @@ SubtypingGraph Subtyping::isSubtype(TypeId subTy, TypeId superTy)
{ {
// any = unknown | error, so we rewrite this to match. // any = unknown | error, so we rewrite this to match.
// As per TAPL: A | B <: T iff A <: T && B <: T // As per TAPL: A | B <: T iff A <: T && B <: T
return isSubtype(builtinTypes->unknownType, superTy).and_(isSubtype(builtinTypes->errorType, superTy)); SubtypingResult result = isSubtype_(builtinTypes->unknownType, superTy);
result.andAlso(isSubtype_(builtinTypes->errorType, superTy));
return result;
} }
else if (auto superUnknown = get<UnknownType>(superTy)) else if (get<UnknownType>(superTy))
{ {
LUAU_ASSERT(!get<AnyType>(subTy)); // TODO: replace with ice. LUAU_ASSERT(!get<AnyType>(subTy)); // TODO: replace with ice.
LUAU_ASSERT(!get<UnionType>(subTy)); // TODO: replace with ice. LUAU_ASSERT(!get<UnionType>(subTy)); // TODO: replace with ice.
@ -98,19 +176,31 @@ SubtypingGraph Subtyping::isSubtype(TypeId subTy, TypeId superTy)
return {false, true}; return {false, true};
else if (get<ErrorType>(subTy)) else if (get<ErrorType>(subTy))
return {false, true}; return {false, true};
else if (auto subGeneric = get<GenericType>(subTy); subGeneric && variance == Variance::Covariant)
{
bool ok = bindGeneric(subTy, superTy);
return {ok};
}
else if (auto superGeneric = get<GenericType>(superTy); superGeneric && variance == Variance::Contravariant)
{
bool ok = bindGeneric(subTy, superTy);
return {ok};
}
else if (auto p = get2<PrimitiveType, PrimitiveType>(subTy, superTy)) else if (auto p = get2<PrimitiveType, PrimitiveType>(subTy, superTy))
return isSubtype(p); return isSubtype_(p);
else if (auto p = get2<SingletonType, PrimitiveType>(subTy, superTy)) else if (auto p = get2<SingletonType, PrimitiveType>(subTy, superTy))
return isSubtype(p); return isSubtype_(p);
else if (auto p = get2<SingletonType, SingletonType>(subTy, superTy)) else if (auto p = get2<SingletonType, SingletonType>(subTy, superTy))
return isSubtype(p); return isSubtype_(p);
else if (auto p = get2<FunctionType, FunctionType>(subTy, superTy)) else if (auto p = get2<FunctionType, FunctionType>(subTy, superTy))
return isSubtype(p); return isSubtype_(p);
else if (auto p = get2<TableType, TableType>(subTy, superTy))
return isSubtype_(p);
return {false}; return {false};
} }
SubtypingGraph Subtyping::isSubtype(TypePackId subTp, TypePackId superTp) SubtypingResult Subtyping::isSubtype_(TypePackId subTp, TypePackId superTp)
{ {
subTp = follow(subTp); subTp = follow(subTp);
superTp = follow(superTp); superTp = follow(superTp);
@ -120,14 +210,17 @@ SubtypingGraph Subtyping::isSubtype(TypePackId subTp, TypePackId superTp)
const size_t headSize = std::min(subHead.size(), superHead.size()); const size_t headSize = std::min(subHead.size(), superHead.size());
std::vector<SubtypingGraph> results; std::vector<SubtypingResult> results;
results.reserve(std::max(subHead.size(), superHead.size()) + 1); results.reserve(std::max(subHead.size(), superHead.size()) + 1);
if (subTp == superTp)
return {true};
// Match head types pairwise // Match head types pairwise
for (size_t i = 0; i < headSize; ++i) for (size_t i = 0; i < headSize; ++i)
{ {
results.push_back(isSubtype(subHead[i], superHead[i])); results.push_back(isSubtype_(subHead[i], superHead[i]));
if (!results.back().isSubtype) if (!results.back().isSubtype)
return {false}; return {false};
} }
@ -141,12 +234,40 @@ SubtypingGraph Subtyping::isSubtype(TypePackId subTp, TypePackId superTp)
if (auto vt = get<VariadicTypePack>(*subTail)) if (auto vt = get<VariadicTypePack>(*subTail))
{ {
for (size_t i = headSize; i < superHead.size(); ++i) for (size_t i = headSize; i < superHead.size(); ++i)
results.push_back(isSubtype_(vt->ty, superHead[i]));
}
else if (auto gt = get<GenericTypePack>(*subTail))
{ {
results.push_back(isSubtype(vt->ty, superHead[i])); if (variance == Variance::Covariant)
{
// For any non-generic type T:
//
// <X>(X) -> () <: (T) -> ()
// Possible optimization: If headSize == 0 then we can just use subTp as-is.
std::vector<TypeId> headSlice(begin(superHead), end(superHead) + headSize);
TypePackId superTailPack = arena->addTypePack(std::move(headSlice), superTail);
if (TypePackId* other = mappedGenericPacks.find(*subTail))
results.push_back(isSubtype_(*other, superTailPack));
else
mappedGenericPacks.try_insert(*subTail, superTailPack);
// FIXME? Not a fan of the early return here. It makes the
// control flow harder to reason about.
return SubtypingResult::all(results);
}
else
{
// For any non-generic type T:
//
// (T) -> () </: <X>(X) -> ()
//
return {false};
} }
} }
else else
LUAU_ASSERT(0); // TODO unexpected(*subTail);
} }
else else
return {false}; return {false};
@ -158,20 +279,43 @@ SubtypingGraph Subtyping::isSubtype(TypePackId subTp, TypePackId superTp)
if (auto vt = get<VariadicTypePack>(*superTail)) if (auto vt = get<VariadicTypePack>(*superTail))
{ {
for (size_t i = headSize; i < subHead.size(); ++i) for (size_t i = headSize; i < subHead.size(); ++i)
results.push_back(isSubtype_(subHead[i], vt->ty));
}
else if (auto gt = get<GenericTypePack>(*superTail))
{ {
results.push_back(isSubtype(subHead[i], vt->ty)); if (variance == Variance::Contravariant)
{
// For any non-generic type T:
//
// <X...>(X...) -> () <: (T) -> ()
// Possible optimization: If headSize == 0 then we can just use subTp as-is.
std::vector<TypeId> headSlice(begin(subHead), end(subHead) + headSize);
TypePackId subTailPack = arena->addTypePack(std::move(headSlice), subTail);
if (TypePackId* other = mappedGenericPacks.find(*superTail))
results.push_back(isSubtype_(*other, subTailPack));
else
mappedGenericPacks.try_insert(*superTail, subTailPack);
// FIXME? Not a fan of the early return here. It makes the
// control flow harder to reason about.
return SubtypingResult::all(results);
}
else
{
// For any non-generic type T:
//
// () -> T </: <X...>() -> X...
return {false};
} }
} }
else else
LUAU_ASSERT(0); // TODO unexpected(*superTail);
} }
else else
return {false}; return {false};
} }
else
{
// subHead and superHead are the same size. Nothing more must be done.
}
// Handle tails // Handle tails
@ -179,10 +323,43 @@ SubtypingGraph Subtyping::isSubtype(TypePackId subTp, TypePackId superTp)
{ {
if (auto p = get2<VariadicTypePack, VariadicTypePack>(*subTail, *superTail)) if (auto p = get2<VariadicTypePack, VariadicTypePack>(*subTail, *superTail))
{ {
results.push_back(isSubtype(p.first->ty, p.second->ty)); results.push_back(isSubtype_(p.first->ty, p.second->ty));
}
else if (auto p = get2<GenericTypePack, GenericTypePack>(*subTail, *superTail))
{
bool ok = bindGeneric(*subTail, *superTail);
results.push_back({ok});
}
else if (get2<VariadicTypePack, GenericTypePack>(*subTail, *superTail))
{
if (variance == Variance::Contravariant)
{
// <A...>(A...) -> number <: (...number) -> number
bool ok = bindGeneric(*subTail, *superTail);
results.push_back({ok});
} }
else else
LUAU_ASSERT(0); // TODO {
// (number) -> ...number </: <A...>(number) -> A...
results.push_back({false});
}
}
else if (get2<GenericTypePack, VariadicTypePack>(*subTail, *superTail))
{
if (variance == Variance::Contravariant)
{
// (...number) -> number </: <A...>(A...) -> number
results.push_back({false});
}
else
{
// <A...>() -> A... <: () -> ...number
bool ok = bindGeneric(*subTail, *superTail);
results.push_back({ok});
}
}
else
iceReporter->ice(format("Subtyping::isSubtype got unexpected type packs %s and %s", toString(*subTail).c_str(), toString(*superTail).c_str()));
} }
else if (subTail) else if (subTail)
{ {
@ -190,8 +367,13 @@ SubtypingGraph Subtyping::isSubtype(TypePackId subTp, TypePackId superTp)
{ {
return {false}; return {false};
} }
else if (get<GenericTypePack>(*subTail))
LUAU_ASSERT(0); // TODO {
bool ok = bindGeneric(*subTail, builtinTypes->emptyTypePack);
return {ok};
}
else
unexpected(*subTail);
} }
else if (superTail) else if (superTail)
{ {
@ -207,17 +389,27 @@ SubtypingGraph Subtyping::isSubtype(TypePackId subTp, TypePackId superTp)
* All variadic type packs are therefore supertypes of the empty type pack. * All variadic type packs are therefore supertypes of the empty type pack.
*/ */
} }
else if (get<GenericTypePack>(*superTail))
{
if (variance == Variance::Contravariant)
{
bool ok = bindGeneric(builtinTypes->emptyTypePack, *superTail);
results.push_back({ok});
}
else
results.push_back({false});
}
else else
LUAU_ASSERT(0); // TODO LUAU_ASSERT(0); // TODO
} }
return SubtypingGraph::and_(results); return SubtypingResult::all(results);
} }
template<typename SubTy, typename SuperTy> template<typename SubTy, typename SuperTy>
SubtypingGraph Subtyping::isSubtype(const TryPair<const SubTy*, const SuperTy*>& pair) SubtypingResult Subtyping::isSubtype_(const TryPair<const SubTy*, const SuperTy*>& pair)
{ {
return isSubtype(pair.first, pair.second); return isSubtype_(pair.first, pair.second);
} }
/* /*
@ -251,49 +443,49 @@ SubtypingGraph Subtyping::isSubtype(const TryPair<const SubTy*, const SuperTy*>&
* other just asks for boolean ~ 'b. We can dispatch this and only commit * other just asks for boolean ~ 'b. We can dispatch this and only commit
* boolean ~ 'b. This constraint does not teach us anything about 'a. * boolean ~ 'b. This constraint does not teach us anything about 'a.
*/ */
SubtypingGraph Subtyping::isSubtype(TypeId subTy, const UnionType* superUnion) SubtypingResult Subtyping::isSubtype_(TypeId subTy, const UnionType* superUnion)
{ {
// As per TAPL: T <: A | B iff T <: A || T <: B // As per TAPL: T <: A | B iff T <: A || T <: B
std::vector<SubtypingGraph> subtypings; std::vector<SubtypingResult> subtypings;
for (TypeId ty : superUnion) for (TypeId ty : superUnion)
subtypings.push_back(isSubtype(subTy, ty)); subtypings.push_back(isSubtype_(subTy, ty));
return SubtypingGraph::or_(subtypings); return SubtypingResult::any(subtypings);
} }
SubtypingGraph Subtyping::isSubtype(const UnionType* subUnion, TypeId superTy) SubtypingResult Subtyping::isSubtype_(const UnionType* subUnion, TypeId superTy)
{ {
// As per TAPL: A | B <: T iff A <: T && B <: T // As per TAPL: A | B <: T iff A <: T && B <: T
std::vector<SubtypingGraph> subtypings; std::vector<SubtypingResult> subtypings;
for (TypeId ty : subUnion) for (TypeId ty : subUnion)
subtypings.push_back(isSubtype(ty, superTy)); subtypings.push_back(isSubtype_(ty, superTy));
return SubtypingGraph::and_(subtypings); return SubtypingResult::all(subtypings);
} }
SubtypingGraph Subtyping::isSubtype(TypeId subTy, const IntersectionType* superIntersection) SubtypingResult Subtyping::isSubtype_(TypeId subTy, const IntersectionType* superIntersection)
{ {
// As per TAPL: T <: A & B iff T <: A && T <: B // As per TAPL: T <: A & B iff T <: A && T <: B
std::vector<SubtypingGraph> subtypings; std::vector<SubtypingResult> subtypings;
for (TypeId ty : superIntersection) for (TypeId ty : superIntersection)
subtypings.push_back(isSubtype(subTy, ty)); subtypings.push_back(isSubtype_(subTy, ty));
return SubtypingGraph::and_(subtypings); return SubtypingResult::all(subtypings);
} }
SubtypingGraph Subtyping::isSubtype(const IntersectionType* subIntersection, TypeId superTy) SubtypingResult Subtyping::isSubtype_(const IntersectionType* subIntersection, TypeId superTy)
{ {
// TODO: Semantic subtyping here. // TODO: Semantic subtyping here.
// As per TAPL: A & B <: T iff A <: T || B <: T // As per TAPL: A & B <: T iff A <: T || B <: T
std::vector<SubtypingGraph> subtypings; std::vector<SubtypingResult> subtypings;
for (TypeId ty : subIntersection) for (TypeId ty : subIntersection)
subtypings.push_back(isSubtype(ty, superTy)); subtypings.push_back(isSubtype_(ty, superTy));
return SubtypingGraph::or_(subtypings); return SubtypingResult::any(subtypings);
} }
SubtypingGraph Subtyping::isSubtype(const PrimitiveType* subPrim, const PrimitiveType* superPrim) SubtypingResult Subtyping::isSubtype_(const PrimitiveType* subPrim, const PrimitiveType* superPrim)
{ {
return {subPrim->type == superPrim->type}; return {subPrim->type == superPrim->type};
} }
SubtypingGraph Subtyping::isSubtype(const SingletonType* subSingleton, const PrimitiveType* superPrim) SubtypingResult Subtyping::isSubtype_(const SingletonType* subSingleton, const PrimitiveType* superPrim)
{ {
if (get<StringSingleton>(subSingleton) && superPrim->type == PrimitiveType::String) if (get<StringSingleton>(subSingleton) && superPrim->type == PrimitiveType::String)
return {true}; return {true};
@ -303,42 +495,123 @@ SubtypingGraph Subtyping::isSubtype(const SingletonType* subSingleton, const Pri
return {false}; return {false};
} }
SubtypingGraph Subtyping::isSubtype(const SingletonType* subSingleton, const SingletonType* superSingleton) SubtypingResult Subtyping::isSubtype_(const SingletonType* subSingleton, const SingletonType* superSingleton)
{ {
return {*subSingleton == *superSingleton}; return {*subSingleton == *superSingleton};
} }
SubtypingGraph Subtyping::isSubtype(const FunctionType* subFunction, const FunctionType* superFunction) SubtypingResult Subtyping::isSubtype_(const TableType* subTable, const TableType* superTable)
{ {
SubtypingGraph argResult = isSubtype(superFunction->argTypes, subFunction->argTypes); SubtypingResult result{true};
SubtypingGraph retResult = isSubtype(subFunction->retTypes, superFunction->retTypes);
return argResult.and_(retResult); for (const auto& [name, prop]: superTable->props)
} {
auto it = subTable->props.find(name);
SubtypingGraph Subtyping::isSubtype(const NormalizedType* subNorm, const NormalizedType* superNorm) if (it != subTable->props.end())
{ {
if (!subNorm || !superNorm) // Table properties are invariant
return {false, true, true}; result.andAlso(isSubtype(it->second.type(), prop.type()));
result.andAlso(isSubtype(prop.type(), it->second.type()));
SubtypingGraph result{true}; }
result = result.and_(isSubtype(subNorm->tops, superNorm->tops)); else
result = result.and_(isSubtype(subNorm->booleans, superNorm->booleans)); return SubtypingResult{false};
// isSubtype(subNorm->classes, superNorm->classes); }
// isSubtype(subNorm->classes, superNorm->tables);
result = result.and_(isSubtype(subNorm->errors, superNorm->errors));
result = result.and_(isSubtype(subNorm->nils, superNorm->nils));
result = result.and_(isSubtype(subNorm->numbers, superNorm->numbers));
result.isSubtype &= Luau::isSubtype(subNorm->strings, superNorm->strings);
// isSubtype(subNorm->strings, superNorm->tables);
result = result.and_(isSubtype(subNorm->threads, superNorm->threads));
// isSubtype(subNorm->tables, superNorm->tables);
// isSubtype(subNorm->tables, superNorm->strings);
// isSubtype(subNorm->tables, superNorm->classes);
// isSubtype(subNorm->functions, superNorm->functions);
// isSubtype(subNorm->tyvars, superNorm->tyvars);
return result; return result;
} }
SubtypingResult Subtyping::isSubtype_(const FunctionType* subFunction, const FunctionType* superFunction)
{
SubtypingResult result;
{
VarianceFlipper vf{&variance};
result.orElse(isSubtype_(superFunction->argTypes, subFunction->argTypes));
}
result.andAlso(isSubtype_(subFunction->retTypes, superFunction->retTypes));
return result;
}
SubtypingResult Subtyping::isSubtype_(const NormalizedType* subNorm, const NormalizedType* superNorm)
{
if (!subNorm || !superNorm)
return {false, true, true};
SubtypingResult result = isSubtype_(subNorm->tops, superNorm->tops);
result.andAlso(isSubtype_(subNorm->booleans, superNorm->booleans));
// isSubtype_(subNorm->classes, superNorm->classes);
// isSubtype_(subNorm->classes, superNorm->tables);
result.andAlso(isSubtype_(subNorm->errors, superNorm->errors));
result.andAlso(isSubtype_(subNorm->nils, superNorm->nils));
result.andAlso(isSubtype_(subNorm->numbers, superNorm->numbers));
result.isSubtype &= Luau::isSubtype(subNorm->strings, superNorm->strings);
// isSubtype_(subNorm->strings, superNorm->tables);
result.andAlso(isSubtype_(subNorm->threads, superNorm->threads));
// isSubtype_(subNorm->tables, superNorm->tables);
// isSubtype_(subNorm->tables, superNorm->strings);
// isSubtype_(subNorm->tables, superNorm->classes);
// isSubtype_(subNorm->functions, superNorm->functions);
// isSubtype_(subNorm->tyvars, superNorm->tyvars);
return result;
}
bool Subtyping::bindGeneric(TypeId subTy, TypeId superTy)
{
if (variance == Variance::Covariant)
{
if (!get<GenericType>(subTy))
return false;
mappedGenerics[subTy].upperBound.insert(superTy);
}
else
{
if (!get<GenericType>(superTy))
return false;
mappedGenerics[superTy].lowerBound.insert(subTy);
}
return true;
}
/*
* If, when performing a subtyping test, we encounter a generic on the left
* side, it is permissible to tentatively bind that generic to the right side
* type.
*/
bool Subtyping::bindGeneric(TypePackId subTp, TypePackId superTp)
{
if (variance == Variance::Contravariant)
std::swap(superTp, subTp);
if (!get<GenericTypePack>(subTp))
return false;
if (TypePackId* m = mappedGenericPacks.find(subTp))
return *m == superTp;
mappedGenericPacks[subTp] = superTp;
return true;
}
template <typename T, typename Container>
TypeId Subtyping::makeAggregateType(const Container& container, TypeId orElse)
{
if (container.empty())
return orElse;
else if (container.size() == 1)
return *begin(container);
else
return arena->addType(T{std::vector<TypeId>(begin(container), end(container))});
}
void Subtyping::unexpected(TypePackId tp)
{
iceReporter->ice(format("Unexpected type pack %s", toString(tp).c_str()));
}
} // namespace Luau } // namespace Luau

View File

@ -36,7 +36,6 @@ LUAU_FASTFLAGVARIABLE(DebugLuauFreezeDuringUnification, false)
LUAU_FASTFLAGVARIABLE(DebugLuauSharedSelf, false) LUAU_FASTFLAGVARIABLE(DebugLuauSharedSelf, false)
LUAU_FASTFLAG(LuauInstantiateInSubtyping) LUAU_FASTFLAG(LuauInstantiateInSubtyping)
LUAU_FASTFLAGVARIABLE(LuauAllowIndexClassParameters, false) LUAU_FASTFLAGVARIABLE(LuauAllowIndexClassParameters, false)
LUAU_FASTFLAGVARIABLE(LuauFixCyclicModuleExports, false)
LUAU_FASTFLAG(LuauOccursIsntAlwaysFailure) LUAU_FASTFLAG(LuauOccursIsntAlwaysFailure)
LUAU_FASTFLAGVARIABLE(LuauTinyControlFlowAnalysis, false) LUAU_FASTFLAGVARIABLE(LuauTinyControlFlowAnalysis, false)
LUAU_FASTFLAGVARIABLE(LuauAlwaysCommitInferencesOfFunctionCalls, false) LUAU_FASTFLAGVARIABLE(LuauAlwaysCommitInferencesOfFunctionCalls, false)
@ -1195,8 +1194,6 @@ ControlFlow TypeChecker::check(const ScopePtr& scope, const AstStatLocal& local)
scope->importedTypeBindings[name] = module->exportedTypeBindings; scope->importedTypeBindings[name] = module->exportedTypeBindings;
scope->importedModules[name] = moduleInfo->name; scope->importedModules[name] = moduleInfo->name;
if (FFlag::LuauFixCyclicModuleExports)
{
// Imported types of requires that transitively refer to current module have to be replaced with 'any' // Imported types of requires that transitively refer to current module have to be replaced with 'any'
for (const auto& [location, path] : requireCycles) for (const auto& [location, path] : requireCycles)
{ {
@ -1207,7 +1204,6 @@ ControlFlow TypeChecker::check(const ScopePtr& scope, const AstStatLocal& local)
} }
} }
} }
}
// In non-strict mode we force the module type on the variable, in strict mode it is already unified // In non-strict mode we force the module type on the variable, in strict mode it is already unified
if (isNonstrictMode()) if (isNonstrictMode())

View File

@ -605,6 +605,10 @@ void Unifier::tryUnify_(TypeId subTy, TypeId superTy, bool isFunctionCall, bool
{ {
// TODO: there are probably cheaper ways to check if any <: T. // TODO: there are probably cheaper ways to check if any <: T.
const NormalizedType* superNorm = normalizer->normalize(superTy); const NormalizedType* superNorm = normalizer->normalize(superTy);
if (!superNorm)
return reportError(location, UnificationTooComplex{});
if (!log.get<AnyType>(superNorm->tops)) if (!log.get<AnyType>(superNorm->tops))
failure = true; failure = true;
} }

View File

@ -272,11 +272,18 @@ class AstExprConstantString : public AstExpr
public: public:
LUAU_RTTI(AstExprConstantString) LUAU_RTTI(AstExprConstantString)
AstExprConstantString(const Location& location, const AstArray<char>& value); enum QuoteStyle
{
Quoted,
Unquoted
};
AstExprConstantString(const Location& location, const AstArray<char>& value, QuoteStyle quoteStyle = Quoted);
void visit(AstVisitor* visitor) override; void visit(AstVisitor* visitor) override;
AstArray<char> value; AstArray<char> value;
QuoteStyle quoteStyle = Quoted;
}; };
class AstExprLocal : public AstExpr class AstExprLocal : public AstExpr

View File

@ -204,7 +204,9 @@ private:
Position position() const; Position position() const;
// consume() assumes current character is not a newline for performance; when that is not known, consumeAny() should be used instead.
void consume(); void consume();
void consumeAny();
Lexeme readCommentBody(); Lexeme readCommentBody();

View File

@ -62,9 +62,10 @@ void AstExprConstantNumber::visit(AstVisitor* visitor)
visitor->visit(this); visitor->visit(this);
} }
AstExprConstantString::AstExprConstantString(const Location& location, const AstArray<char>& value) AstExprConstantString::AstExprConstantString(const Location& location, const AstArray<char>& value, QuoteStyle quoteStyle)
: AstExpr(ClassIndex(), location) : AstExpr(ClassIndex(), location)
, value(value) , value(value)
, quoteStyle(quoteStyle)
{ {
} }

View File

@ -6,6 +6,8 @@
#include <limits.h> #include <limits.h>
LUAU_FASTFLAGVARIABLE(LuauLexerConsumeFast, false)
namespace Luau namespace Luau
{ {
@ -373,7 +375,7 @@ const Lexeme& Lexer::next(bool skipComments, bool updatePrevLocation)
{ {
// consume whitespace before the token // consume whitespace before the token
while (isSpace(peekch())) while (isSpace(peekch()))
consume(); consumeAny();
if (updatePrevLocation) if (updatePrevLocation)
prevLocation = lexeme.location; prevLocation = lexeme.location;
@ -438,7 +440,28 @@ Position Lexer::position() const
return Position(line, offset - lineOffset); return Position(line, offset - lineOffset);
} }
LUAU_FORCEINLINE
void Lexer::consume() void Lexer::consume()
{
if (isNewline(buffer[offset]))
{
// TODO: When the flag is removed, remove the outer condition
if (FFlag::LuauLexerConsumeFast)
{
LUAU_ASSERT(!isNewline(buffer[offset]));
}
else
{
line++;
lineOffset = offset + 1;
}
}
offset++;
}
LUAU_FORCEINLINE
void Lexer::consumeAny()
{ {
if (isNewline(buffer[offset])) if (isNewline(buffer[offset]))
{ {
@ -524,7 +547,7 @@ Lexeme Lexer::readLongString(const Position& start, int sep, Lexeme::Type ok, Le
} }
else else
{ {
consume(); consumeAny();
} }
} }
@ -540,7 +563,7 @@ void Lexer::readBackslashInString()
case '\r': case '\r':
consume(); consume();
if (peekch() == '\n') if (peekch() == '\n')
consume(); consumeAny();
break; break;
case 0: case 0:
@ -549,11 +572,11 @@ void Lexer::readBackslashInString()
case 'z': case 'z':
consume(); consume();
while (isSpace(peekch())) while (isSpace(peekch()))
consume(); consumeAny();
break; break;
default: default:
consume(); consumeAny();
} }
} }
@ -939,6 +962,9 @@ Lexeme Lexer::readNext()
case ';': case ';':
case ',': case ',':
case '#': case '#':
case '?':
case '&':
case '|':
{ {
char ch = peekch(); char ch = peekch();
consume(); consume();

View File

@ -15,8 +15,6 @@ LUAU_FASTINTVARIABLE(LuauRecursionLimit, 1000)
LUAU_FASTINTVARIABLE(LuauParseErrorLimit, 100) LUAU_FASTINTVARIABLE(LuauParseErrorLimit, 100)
LUAU_FASTFLAGVARIABLE(LuauParseDeclareClassIndexer, false) LUAU_FASTFLAGVARIABLE(LuauParseDeclareClassIndexer, false)
#define ERROR_INVALID_INTERP_DOUBLE_BRACE "Double braces are not permitted within interpolated strings. Did you mean '\\{'?"
namespace Luau namespace Luau
{ {
@ -899,13 +897,13 @@ AstStat* Parser::parseDeclaration(const Location& start)
expectAndConsume(':', "property type annotation"); expectAndConsume(':', "property type annotation");
AstType* type = parseType(); AstType* type = parseType();
// TODO: since AstName conains a char*, it can't contain null // since AstName contains a char*, it can't contain null
bool containsNull = chars && (strnlen(chars->data, chars->size) < chars->size); bool containsNull = chars && (strnlen(chars->data, chars->size) < chars->size);
if (chars && !containsNull) if (chars && !containsNull)
props.push_back(AstDeclaredClassProp{AstName(chars->data), type, false}); props.push_back(AstDeclaredClassProp{AstName(chars->data), type, false});
else else
report(begin.location, "String literal contains malformed escape sequence"); report(begin.location, "String literal contains malformed escape sequence or \\0");
} }
else if (lexer.current().type == '[' && FFlag::LuauParseDeclareClassIndexer) else if (lexer.current().type == '[' && FFlag::LuauParseDeclareClassIndexer)
{ {
@ -1328,13 +1326,13 @@ AstType* Parser::parseTableType()
AstType* type = parseType(); AstType* type = parseType();
// TODO: since AstName conains a char*, it can't contain null // since AstName contains a char*, it can't contain null
bool containsNull = chars && (strnlen(chars->data, chars->size) < chars->size); bool containsNull = chars && (strnlen(chars->data, chars->size) < chars->size);
if (chars && !containsNull) if (chars && !containsNull)
props.push_back({AstName(chars->data), begin.location, type}); props.push_back({AstName(chars->data), begin.location, type});
else else
report(begin.location, "String literal contains malformed escape sequence"); report(begin.location, "String literal contains malformed escape sequence or \\0");
} }
else if (lexer.current().type == '[') else if (lexer.current().type == '[')
{ {
@ -1622,7 +1620,7 @@ AstTypeOrPack Parser::parseSimpleType(bool allowPack)
else if (lexer.current().type == Lexeme::BrokenString) else if (lexer.current().type == Lexeme::BrokenString)
{ {
nextLexeme(); nextLexeme();
return {reportTypeError(start, {}, "Malformed string")}; return {reportTypeError(start, {}, "Malformed string; did you forget to finish it?")};
} }
else if (lexer.current().type == Lexeme::Name) else if (lexer.current().type == Lexeme::Name)
{ {
@ -1741,7 +1739,8 @@ AstTypePack* Parser::parseTypePack()
return allocator.alloc<AstTypePackGeneric>(Location(name.location, end), name.name); return allocator.alloc<AstTypePackGeneric>(Location(name.location, end), name.name);
} }
// No type pack annotation exists here. // TODO: shouldParseTypePack can be removed and parseTypePack can be called unconditionally instead
LUAU_ASSERT(!"parseTypePack can't be called if shouldParseTypePack() returned false");
return nullptr; return nullptr;
} }
@ -1826,7 +1825,7 @@ std::optional<AstExprUnary::Op> Parser::checkUnaryConfusables()
if (curr.type == '!') if (curr.type == '!')
{ {
report(start, "Unexpected '!', did you mean 'not'?"); report(start, "Unexpected '!'; did you mean 'not'?");
return AstExprUnary::Not; return AstExprUnary::Not;
} }
@ -1848,20 +1847,20 @@ std::optional<AstExprBinary::Op> Parser::checkBinaryConfusables(const BinaryOpPr
if (curr.type == '&' && next.type == '&' && curr.location.end == next.location.begin && binaryPriority[AstExprBinary::And].left > limit) if (curr.type == '&' && next.type == '&' && curr.location.end == next.location.begin && binaryPriority[AstExprBinary::And].left > limit)
{ {
nextLexeme(); nextLexeme();
report(Location(start, next.location), "Unexpected '&&', did you mean 'and'?"); report(Location(start, next.location), "Unexpected '&&'; did you mean 'and'?");
return AstExprBinary::And; return AstExprBinary::And;
} }
else if (curr.type == '|' && next.type == '|' && curr.location.end == next.location.begin && binaryPriority[AstExprBinary::Or].left > limit) else if (curr.type == '|' && next.type == '|' && curr.location.end == next.location.begin && binaryPriority[AstExprBinary::Or].left > limit)
{ {
nextLexeme(); nextLexeme();
report(Location(start, next.location), "Unexpected '||', did you mean 'or'?"); report(Location(start, next.location), "Unexpected '||'; did you mean 'or'?");
return AstExprBinary::Or; return AstExprBinary::Or;
} }
else if (curr.type == '!' && next.type == '=' && curr.location.end == next.location.begin && else if (curr.type == '!' && next.type == '=' && curr.location.end == next.location.begin &&
binaryPriority[AstExprBinary::CompareNe].left > limit) binaryPriority[AstExprBinary::CompareNe].left > limit)
{ {
nextLexeme(); nextLexeme();
report(Location(start, next.location), "Unexpected '!=', did you mean '~='?"); report(Location(start, next.location), "Unexpected '!='; did you mean '~='?");
return AstExprBinary::CompareNe; return AstExprBinary::CompareNe;
} }
@ -2169,12 +2168,12 @@ AstExpr* Parser::parseSimpleExpr()
else if (lexer.current().type == Lexeme::BrokenString) else if (lexer.current().type == Lexeme::BrokenString)
{ {
nextLexeme(); nextLexeme();
return reportExprError(start, {}, "Malformed string"); return reportExprError(start, {}, "Malformed string; did you forget to finish it?");
} }
else if (lexer.current().type == Lexeme::BrokenInterpDoubleBrace) else if (lexer.current().type == Lexeme::BrokenInterpDoubleBrace)
{ {
nextLexeme(); nextLexeme();
return reportExprError(start, {}, ERROR_INVALID_INTERP_DOUBLE_BRACE); return reportExprError(start, {}, "Double braces are not permitted within interpolated strings; did you mean '\\{'?");
} }
else if (lexer.current().type == Lexeme::Dot3) else if (lexer.current().type == Lexeme::Dot3)
{ {
@ -2312,7 +2311,7 @@ AstExpr* Parser::parseTableConstructor()
nameString.data = const_cast<char*>(name.name.value); nameString.data = const_cast<char*>(name.name.value);
nameString.size = strlen(name.name.value); nameString.size = strlen(name.name.value);
AstExpr* key = allocator.alloc<AstExprConstantString>(name.location, nameString); AstExpr* key = allocator.alloc<AstExprConstantString>(name.location, nameString, AstExprConstantString::Unquoted);
AstExpr* value = parseExpr(); AstExpr* value = parseExpr();
if (AstExprFunction* func = value->as<AstExprFunction>()) if (AstExprFunction* func = value->as<AstExprFunction>())
@ -2661,7 +2660,7 @@ AstExpr* Parser::parseInterpString()
{ {
errorWhileChecking = true; errorWhileChecking = true;
nextLexeme(); nextLexeme();
expressions.push_back(reportExprError(endLocation, {}, "Malformed interpolated string, did you forget to add a '`'?")); expressions.push_back(reportExprError(endLocation, {}, "Malformed interpolated string; did you forget to add a '`'?"));
break; break;
} }
default: default:
@ -2681,10 +2680,10 @@ AstExpr* Parser::parseInterpString()
break; break;
case Lexeme::BrokenInterpDoubleBrace: case Lexeme::BrokenInterpDoubleBrace:
nextLexeme(); nextLexeme();
return reportExprError(endLocation, {}, ERROR_INVALID_INTERP_DOUBLE_BRACE); return reportExprError(endLocation, {}, "Double braces are not permitted within interpolated strings; did you mean '\\{'?");
case Lexeme::BrokenString: case Lexeme::BrokenString:
nextLexeme(); nextLexeme();
return reportExprError(endLocation, {}, "Malformed interpolated string, did you forget to add a '}'?"); return reportExprError(endLocation, {}, "Malformed interpolated string; did you forget to add a '}'?");
default: default:
return reportExprError(endLocation, {}, "Malformed interpolated string, got %s", lexer.current().toString().c_str()); return reportExprError(endLocation, {}, "Malformed interpolated string, got %s", lexer.current().toString().c_str());
} }

View File

@ -757,14 +757,6 @@ int replMain(int argc, char** argv)
} }
#endif #endif
#if !LUA_CUSTOM_EXECUTION
if (codegen)
{
fprintf(stderr, "To run with --codegen, Luau has to be built with LUA_CUSTOM_EXECUTION enabled\n");
return 1;
}
#endif
if (codegenPerf) if (codegenPerf)
{ {
#if __linux__ #if __linux__
@ -784,10 +776,7 @@ int replMain(int argc, char** argv)
} }
if (codegen && !Luau::CodeGen::isSupported()) if (codegen && !Luau::CodeGen::isSupported())
{ fprintf(stderr, "Warning: Native code generation is not supported in current configuration\n");
fprintf(stderr, "Cannot enable --codegen, native code generation is not supported in current configuration\n");
return 1;
}
const std::vector<std::string> files = getSourceFiles(argc, argv); const std::vector<std::string> files = getSourceFiles(argc, argv);

View File

@ -12,7 +12,6 @@ option(LUAU_BUILD_WEB "Build Web module" OFF)
option(LUAU_WERROR "Warnings as errors" OFF) option(LUAU_WERROR "Warnings as errors" OFF)
option(LUAU_STATIC_CRT "Link with the static CRT (/MT)" OFF) option(LUAU_STATIC_CRT "Link with the static CRT (/MT)" OFF)
option(LUAU_EXTERN_C "Use extern C for all APIs" OFF) option(LUAU_EXTERN_C "Use extern C for all APIs" OFF)
option(LUAU_NATIVE "Enable support for native code generation" OFF)
cmake_policy(SET CMP0054 NEW) cmake_policy(SET CMP0054 NEW)
cmake_policy(SET CMP0091 NEW) cmake_policy(SET CMP0091 NEW)
@ -146,13 +145,7 @@ if(LUAU_EXTERN_C)
target_compile_definitions(Luau.VM PUBLIC LUA_USE_LONGJMP=1) target_compile_definitions(Luau.VM PUBLIC LUA_USE_LONGJMP=1)
target_compile_definitions(Luau.VM PUBLIC LUA_API=extern\"C\") target_compile_definitions(Luau.VM PUBLIC LUA_API=extern\"C\")
target_compile_definitions(Luau.Compiler PUBLIC LUACODE_API=extern\"C\") target_compile_definitions(Luau.Compiler PUBLIC LUACODE_API=extern\"C\")
endif()
if(LUAU_NATIVE)
target_compile_definitions(Luau.VM PUBLIC LUA_CUSTOM_EXECUTION=1)
if(LUAU_EXTERN_C)
target_compile_definitions(Luau.CodeGen PUBLIC LUACODEGEN_API=extern\"C\") target_compile_definitions(Luau.CodeGen PUBLIC LUACODEGEN_API=extern\"C\")
endif()
endif() endif()
if (CMAKE_CXX_COMPILER_ID STREQUAL "MSVC" AND MSVC_VERSION GREATER_EQUAL 1924) if (CMAKE_CXX_COMPILER_ID STREQUAL "MSVC" AND MSVC_VERSION GREATER_EQUAL 1924)

View File

@ -18,6 +18,16 @@ enum CodeGenFlags
CodeGen_OnlyNativeModules = 1 << 0, CodeGen_OnlyNativeModules = 1 << 0,
}; };
enum class CodeGenCompilationResult
{
Success, // Successfully generated code for at least one function
NothingToCompile, // There were no new functions to compile
CodeGenNotInitialized, // Native codegen system is not initialized
CodeGenFailed, // Native codegen failed due to an internal compiler error
AllocationFailed, // Native codegen failed due to an allocation error
};
struct CompilationStats struct CompilationStats
{ {
size_t bytecodeSizeBytes = 0; size_t bytecodeSizeBytes = 0;
@ -36,7 +46,7 @@ void create(lua_State* L, AllocationCallback* allocationCallback, void* allocati
void create(lua_State* L); void create(lua_State* L);
// Builds target function and all inner functions // Builds target function and all inner functions
void compile(lua_State* L, int idx, unsigned int flags = 0, CompilationStats* stats = nullptr); CodeGenCompilationResult compile(lua_State* L, int idx, unsigned int flags = 0, CompilationStats* stats = nullptr);
using AnnotatorFn = void (*)(void* context, std::string& result, int fid, int instpos); using AnnotatorFn = void (*)(void* context, std::string& result, int fid, int instpos);

View File

@ -245,8 +245,8 @@ enum class IrCmd : uint8_t
STRING_LEN, STRING_LEN,
// Allocate new table // Allocate new table
// A: int (array element count) // A: unsigned int (array element count)
// B: int (node element count) // B: unsigned int (node element count)
NEW_TABLE, NEW_TABLE,
// Duplicate a table // Duplicate a table
@ -359,12 +359,6 @@ enum class IrCmd : uint8_t
// C: tag/undef (tag of the value that was written) // C: tag/undef (tag of the value that was written)
SET_UPVALUE, SET_UPVALUE,
// Convert TValues into numbers for a numerical for loop
// A: Rn (start)
// B: Rn (end)
// C: Rn (step)
PREPARE_FORN,
// Guards and checks (these instructions are not block terminators even though they jump to fallback) // Guards and checks (these instructions are not block terminators even though they jump to fallback)
// Guard against tag mismatch // Guard against tag mismatch
@ -463,6 +457,7 @@ enum class IrCmd : uint8_t
// C: Rn (source start) // C: Rn (source start)
// D: int (count or -1 to assign values up to stack top) // D: int (count or -1 to assign values up to stack top)
// E: unsigned int (table index to start from) // E: unsigned int (table index to start from)
// F: undef/unsigned int (target table known size)
SETLIST, SETLIST,
// Call specified function // Call specified function

View File

@ -77,6 +77,7 @@ struct IrRegAllocX64
std::array<uint32_t, 16> gprInstUsers; std::array<uint32_t, 16> gprInstUsers;
std::array<bool, 16> freeXmmMap; std::array<bool, 16> freeXmmMap;
std::array<uint32_t, 16> xmmInstUsers; std::array<uint32_t, 16> xmmInstUsers;
uint8_t usableXmmRegCount = 0;
std::bitset<256> usedSpillSlots; std::bitset<256> usedSpillSlots;
unsigned maxUsedSlot = 0; unsigned maxUsedSlot = 0;

View File

@ -5,6 +5,7 @@
#include "Luau/RegisterX64.h" #include "Luau/RegisterX64.h"
#include <initializer_list> #include <initializer_list>
#include <vector>
#include <stddef.h> #include <stddef.h>
#include <stdint.h> #include <stdint.h>
@ -48,7 +49,8 @@ public:
// mov rbp, rsp // mov rbp, rsp
// push reg in the order specified in regs // push reg in the order specified in regs
// sub rsp, stackSize // sub rsp, stackSize
virtual void prologueX64(uint32_t prologueSize, uint32_t stackSize, bool setupFrame, std::initializer_list<X64::RegisterX64> regs) = 0; virtual void prologueX64(uint32_t prologueSize, uint32_t stackSize, bool setupFrame, std::initializer_list<X64::RegisterX64> gpr,
const std::vector<X64::RegisterX64>& simd) = 0;
virtual size_t getSize() const = 0; virtual size_t getSize() const = 0;
virtual size_t getFunctionCount() const = 0; virtual size_t getFunctionCount() const = 0;

View File

@ -30,7 +30,8 @@ public:
void finishInfo() override; void finishInfo() override;
void prologueA64(uint32_t prologueSize, uint32_t stackSize, std::initializer_list<A64::RegisterA64> regs) override; void prologueA64(uint32_t prologueSize, uint32_t stackSize, std::initializer_list<A64::RegisterA64> regs) override;
void prologueX64(uint32_t prologueSize, uint32_t stackSize, bool setupFrame, std::initializer_list<X64::RegisterX64> regs) override; void prologueX64(uint32_t prologueSize, uint32_t stackSize, bool setupFrame, std::initializer_list<X64::RegisterX64> gpr,
const std::vector<X64::RegisterX64>& simd) override;
size_t getSize() const override; size_t getSize() const override;
size_t getFunctionCount() const override; size_t getFunctionCount() const override;

View File

@ -50,7 +50,8 @@ public:
void finishInfo() override; void finishInfo() override;
void prologueA64(uint32_t prologueSize, uint32_t stackSize, std::initializer_list<A64::RegisterA64> regs) override; void prologueA64(uint32_t prologueSize, uint32_t stackSize, std::initializer_list<A64::RegisterA64> regs) override;
void prologueX64(uint32_t prologueSize, uint32_t stackSize, bool setupFrame, std::initializer_list<X64::RegisterX64> regs) override; void prologueX64(uint32_t prologueSize, uint32_t stackSize, bool setupFrame, std::initializer_list<X64::RegisterX64> gpr,
const std::vector<X64::RegisterX64>& simd) override;
size_t getSize() const override; size_t getSize() const override;
size_t getFunctionCount() const override; size_t getFunctionCount() const override;

View File

@ -162,9 +162,6 @@ unsigned int getCpuFeaturesA64()
bool isSupported() bool isSupported()
{ {
if (!LUA_CUSTOM_EXECUTION)
return false;
if (LUA_EXTRA_SIZE != 1) if (LUA_EXTRA_SIZE != 1)
return false; return false;
@ -247,23 +244,33 @@ void create(lua_State* L)
create(L, nullptr, nullptr); create(L, nullptr, nullptr);
} }
void compile(lua_State* L, int idx, unsigned int flags, CompilationStats* stats) CodeGenCompilationResult compile(lua_State* L, int idx, unsigned int flags, CompilationStats* stats)
{ {
LUAU_ASSERT(lua_isLfunction(L, idx)); LUAU_ASSERT(lua_isLfunction(L, idx));
const TValue* func = luaA_toobject(L, idx); const TValue* func = luaA_toobject(L, idx);
Proto* root = clvalue(func)->l.p;
if ((flags & CodeGen_OnlyNativeModules) != 0 && (root->flags & LPF_NATIVE_MODULE) == 0)
return CodeGenCompilationResult::NothingToCompile;
// If initialization has failed, do not compile any functions // If initialization has failed, do not compile any functions
NativeState* data = getNativeState(L); NativeState* data = getNativeState(L);
if (!data) if (!data)
return; return CodeGenCompilationResult::CodeGenNotInitialized;
Proto* root = clvalue(func)->l.p;
if ((flags & CodeGen_OnlyNativeModules) != 0 && (root->flags & LPF_NATIVE_MODULE) == 0)
return;
std::vector<Proto*> protos; std::vector<Proto*> protos;
gatherFunctions(protos, root); gatherFunctions(protos, root);
// Skip protos that have been compiled during previous invocations of CodeGen::compile
protos.erase(std::remove_if(protos.begin(), protos.end(),
[](Proto* p) {
return p == nullptr || p->execdata != nullptr;
}),
protos.end());
if (protos.empty())
return CodeGenCompilationResult::NothingToCompile;
#if defined(__aarch64__) #if defined(__aarch64__)
static unsigned int cpuFeatures = getCpuFeaturesA64(); static unsigned int cpuFeatures = getCpuFeaturesA64();
A64::AssemblyBuilderA64 build(/* logText= */ false, cpuFeatures); A64::AssemblyBuilderA64 build(/* logText= */ false, cpuFeatures);
@ -281,9 +288,7 @@ void compile(lua_State* L, int idx, unsigned int flags, CompilationStats* stats)
std::vector<NativeProto> results; std::vector<NativeProto> results;
results.reserve(protos.size()); results.reserve(protos.size());
// Skip protos that have been compiled during previous invocations of CodeGen::compile
for (Proto* p : protos) for (Proto* p : protos)
if (p && p->execdata == nullptr)
if (std::optional<NativeProto> np = createNativeFunction(build, helpers, p)) if (std::optional<NativeProto> np = createNativeFunction(build, helpers, p))
results.push_back(*np); results.push_back(*np);
@ -293,12 +298,12 @@ void compile(lua_State* L, int idx, unsigned int flags, CompilationStats* stats)
for (NativeProto result : results) for (NativeProto result : results)
destroyExecData(result.execdata); destroyExecData(result.execdata);
return; return CodeGenCompilationResult::CodeGenFailed;
} }
// If no functions were assembled, we don't need to allocate/copy executable pages for helpers // If no functions were assembled, we don't need to allocate/copy executable pages for helpers
if (results.empty()) if (results.empty())
return; return CodeGenCompilationResult::CodeGenFailed;
uint8_t* nativeData = nullptr; uint8_t* nativeData = nullptr;
size_t sizeNativeData = 0; size_t sizeNativeData = 0;
@ -309,7 +314,7 @@ void compile(lua_State* L, int idx, unsigned int flags, CompilationStats* stats)
for (NativeProto result : results) for (NativeProto result : results)
destroyExecData(result.execdata); destroyExecData(result.execdata);
return; return CodeGenCompilationResult::AllocationFailed;
} }
if (gPerfLogFn && results.size() > 0) if (gPerfLogFn && results.size() > 0)
@ -348,6 +353,8 @@ void compile(lua_State* L, int idx, unsigned int flags, CompilationStats* stats)
stats->nativeCodeSizeBytes += build.code.size(); stats->nativeCodeSizeBytes += build.code.size();
stats->nativeDataSizeBytes += build.data.size(); stats->nativeDataSizeBytes += build.data.size();
} }
return CodeGenCompilationResult::Success;
} }
void setPerfLog(void* context, PerfLogFn logFn) void setPerfLog(void* context, PerfLogFn logFn)

View File

@ -16,10 +16,24 @@
* | rdx home space | (unused) * | rdx home space | (unused)
* | rcx home space | (unused) * | rcx home space | (unused)
* | return address | * | return address |
* | ... saved non-volatile registers ... <-- rsp + kStackSize + kLocalsSize * | ... saved non-volatile registers ... <-- rsp + kStackSizeFull
* | unused | for 16 byte alignment of the stack * | alignment |
* | xmm9 non-vol |
* | xmm9 cont. |
* | xmm8 non-vol |
* | xmm8 cont. |
* | xmm7 non-vol |
* | xmm7 cont. |
* | xmm6 non-vol |
* | xmm6 cont. |
* | spill slot 5 |
* | spill slot 4 |
* | spill slot 3 |
* | spill slot 2 |
* | spill slot 1 | <-- rsp + kStackOffsetToSpillSlots
* | sTemporarySlot |
* | sCode | * | sCode |
* | sClosure | <-- rsp + kStackSize * | sClosure | <-- rsp + kStackOffsetToLocals
* | argument 6 | <-- rsp + 40 * | argument 6 | <-- rsp + 40
* | argument 5 | <-- rsp + 32 * | argument 5 | <-- rsp + 32
* | r9 home space | * | r9 home space |
@ -81,24 +95,43 @@ static EntryLocations buildEntryFunction(AssemblyBuilderX64& build, UnwindBuilde
build.push(rdi); build.push(rdi);
build.push(rsi); build.push(rsi);
// On Windows, rbp is available as a general-purpose non-volatile register; we currently don't use it, but we need to push an even number // On Windows, rbp is available as a general-purpose non-volatile register and this might be freed up
// of registers for stack alignment...
build.push(rbp); build.push(rbp);
// TODO: once we start using non-volatile SIMD registers on Windows, we will save those here
} }
// Allocate stack space (reg home area + local data) // Allocate stack space
build.sub(rsp, kStackSize + kLocalsSize); uint8_t usableXmmRegCount = getXmmRegisterCount(build.abi);
unsigned xmmStorageSize = getNonVolXmmStorageSize(build.abi, usableXmmRegCount);
unsigned fullStackSize = getFullStackSize(build.abi, usableXmmRegCount);
build.sub(rsp, fullStackSize);
OperandX64 xmmStorageOffset = rsp + (fullStackSize - (kStackAlign + xmmStorageSize));
// On Windows, we have to save non-volatile xmm registers
std::vector<RegisterX64> savedXmmRegs;
if (build.abi == ABIX64::Windows)
{
if (usableXmmRegCount > kWindowsFirstNonVolXmmReg)
savedXmmRegs.reserve(usableXmmRegCount - kWindowsFirstNonVolXmmReg);
for (uint8_t i = kWindowsFirstNonVolXmmReg, offset = 0; i < usableXmmRegCount; i++, offset += 16)
{
RegisterX64 xmmReg = RegisterX64{SizeX64::xmmword, i};
build.vmovaps(xmmword[xmmStorageOffset + offset], xmmReg);
savedXmmRegs.push_back(xmmReg);
}
}
locations.prologueEnd = build.setLabel(); locations.prologueEnd = build.setLabel();
uint32_t prologueSize = build.getLabelOffset(locations.prologueEnd) - build.getLabelOffset(locations.start); uint32_t prologueSize = build.getLabelOffset(locations.prologueEnd) - build.getLabelOffset(locations.start);
if (build.abi == ABIX64::SystemV) if (build.abi == ABIX64::SystemV)
unwind.prologueX64(prologueSize, kStackSize + kLocalsSize, /* setupFrame= */ true, {rbx, r12, r13, r14, r15}); unwind.prologueX64(prologueSize, fullStackSize, /* setupFrame= */ true, {rbx, r12, r13, r14, r15}, {});
else if (build.abi == ABIX64::Windows) else if (build.abi == ABIX64::Windows)
unwind.prologueX64(prologueSize, kStackSize + kLocalsSize, /* setupFrame= */ false, {rbx, r12, r13, r14, r15, rdi, rsi, rbp}); unwind.prologueX64(prologueSize, fullStackSize, /* setupFrame= */ false, {rbx, r12, r13, r14, r15, rdi, rsi, rbp}, savedXmmRegs);
// Setup native execution environment // Setup native execution environment
build.mov(rState, rArg1); build.mov(rState, rArg1);
@ -118,8 +151,15 @@ static EntryLocations buildEntryFunction(AssemblyBuilderX64& build, UnwindBuilde
// Even though we jumped away, we will return here in the end // Even though we jumped away, we will return here in the end
locations.epilogueStart = build.setLabel(); locations.epilogueStart = build.setLabel();
// Cleanup and exit // Epilogue and exit
build.add(rsp, kStackSize + kLocalsSize); if (build.abi == ABIX64::Windows)
{
// xmm registers are restored before the official epilogue that has to start with 'add rsp/lea rsp'
for (uint8_t i = kWindowsFirstNonVolXmmReg, offset = 0; i < usableXmmRegCount; i++, offset += 16)
build.vmovaps(RegisterX64{SizeX64::xmmword, i}, xmmword[xmmStorageOffset + offset]);
}
build.add(rsp, fullStackSize);
if (build.abi == ABIX64::Windows) if (build.abi == ABIX64::Windows)
{ {

View File

@ -123,16 +123,6 @@ void callLengthHelper(IrRegAllocX64& regs, AssemblyBuilderX64& build, int ra, in
emitUpdateBase(build); emitUpdateBase(build);
} }
void callPrepareForN(IrRegAllocX64& regs, AssemblyBuilderX64& build, int limit, int step, int init)
{
IrCallWrapperX64 callWrap(regs, build);
callWrap.addArgument(SizeX64::qword, rState);
callWrap.addArgument(SizeX64::qword, luauRegAddress(limit));
callWrap.addArgument(SizeX64::qword, luauRegAddress(step));
callWrap.addArgument(SizeX64::qword, luauRegAddress(init));
callWrap.call(qword[rNativeContext + offsetof(NativeContext, luaV_prepareFORN)]);
}
void callGetTable(IrRegAllocX64& regs, AssemblyBuilderX64& build, int rb, OperandX64 c, int ra) void callGetTable(IrRegAllocX64& regs, AssemblyBuilderX64& build, int rb, OperandX64 c, int ra)
{ {
IrCallWrapperX64 callWrap(regs, build); IrCallWrapperX64 callWrap(regs, build);

View File

@ -42,16 +42,55 @@ constexpr RegisterX64 rBase = r14; // StkId base
constexpr RegisterX64 rNativeContext = r13; // NativeContext* context constexpr RegisterX64 rNativeContext = r13; // NativeContext* context
constexpr RegisterX64 rConstants = r12; // TValue* k constexpr RegisterX64 rConstants = r12; // TValue* k
// Native code is as stackless as the interpreter, so we can place some data on the stack once and have it accessible at any point constexpr unsigned kExtraLocals = 3; // Number of 8 byte slots available for specialized local variables specified below
// See CodeGenX64.cpp for layout constexpr unsigned kSpillSlots = 5; // Number of 8 byte slots available for register allocator to spill data into
constexpr unsigned kStackSize = 32 + 16; // 4 home locations for registers, 16 bytes for additional function call arguments static_assert((kExtraLocals + kSpillSlots) * 8 % 16 == 0, "locals have to preserve 16 byte alignment");
constexpr unsigned kSpillSlots = 4; // locations for register allocator to spill data into
constexpr unsigned kLocalsSize = 24 + 8 * kSpillSlots; // 3 extra slots for our custom locals (also aligns the stack to 16 byte boundary)
constexpr OperandX64 sClosure = qword[rsp + kStackSize + 0]; // Closure* cl constexpr uint8_t kWindowsFirstNonVolXmmReg = 6;
constexpr OperandX64 sCode = qword[rsp + kStackSize + 8]; // Instruction* code
constexpr OperandX64 sTemporarySlot = addr[rsp + kStackSize + 16]; constexpr uint8_t kWindowsUsableXmmRegs = 10; // Some xmm regs are non-volatile, we have to balance how many we want to use/preserve
constexpr OperandX64 sSpillArea = addr[rsp + kStackSize + 24]; constexpr uint8_t kSystemVUsableXmmRegs = 16; // All xmm regs are volatile
inline uint8_t getXmmRegisterCount(ABIX64 abi)
{
return abi == ABIX64::SystemV ? kSystemVUsableXmmRegs : kWindowsUsableXmmRegs;
}
// Native code is as stackless as the interpreter, so we can place some data on the stack once and have it accessible at any point
// Stack is separated into sections for different data. See CodeGenX64.cpp for layout overview
constexpr unsigned kStackAlign = 8; // Bytes we need to align the stack for non-vol xmm register storage
constexpr unsigned kStackLocalStorage = 8 * kExtraLocals;
constexpr unsigned kStackSpillStorage = 8 * kSpillSlots;
constexpr unsigned kStackExtraArgumentStorage = 2 * 8; // Bytes for 5th and 6th function call arguments used under Windows ABI
constexpr unsigned kStackRegHomeStorage = 4 * 8; // Register 'home' locations that can be used by callees under Windows ABI
inline unsigned getNonVolXmmStorageSize(ABIX64 abi, uint8_t xmmRegCount)
{
if (abi == ABIX64::SystemV)
return 0;
// First 6 are volatile
if (xmmRegCount <= kWindowsFirstNonVolXmmReg)
return 0;
LUAU_ASSERT(xmmRegCount <= 16);
return (xmmRegCount - kWindowsFirstNonVolXmmReg) * 16;
}
// Useful offsets to specific parts
constexpr unsigned kStackOffsetToLocals = kStackExtraArgumentStorage + kStackRegHomeStorage;
constexpr unsigned kStackOffsetToSpillSlots = kStackOffsetToLocals + kStackLocalStorage;
inline unsigned getFullStackSize(ABIX64 abi, uint8_t xmmRegCount)
{
return kStackOffsetToSpillSlots + kStackSpillStorage + getNonVolXmmStorageSize(abi, xmmRegCount) + kStackAlign;
}
constexpr OperandX64 sClosure = qword[rsp + kStackOffsetToLocals + 0]; // Closure* cl
constexpr OperandX64 sCode = qword[rsp + kStackOffsetToLocals + 8]; // Instruction* code
constexpr OperandX64 sTemporarySlot = addr[rsp + kStackOffsetToLocals + 16];
constexpr OperandX64 sSpillArea = addr[rsp + kStackOffsetToSpillSlots];
inline OperandX64 luauReg(int ri) inline OperandX64 luauReg(int ri)
{ {
@ -161,7 +200,6 @@ void convertNumberToIndexOrJump(AssemblyBuilderX64& build, RegisterX64 tmp, Regi
void callArithHelper(IrRegAllocX64& regs, AssemblyBuilderX64& build, int ra, int rb, OperandX64 c, TMS tm); void callArithHelper(IrRegAllocX64& regs, AssemblyBuilderX64& build, int ra, int rb, OperandX64 c, TMS tm);
void callLengthHelper(IrRegAllocX64& regs, AssemblyBuilderX64& build, int ra, int rb); void callLengthHelper(IrRegAllocX64& regs, AssemblyBuilderX64& build, int ra, int rb);
void callPrepareForN(IrRegAllocX64& regs, AssemblyBuilderX64& build, int limit, int step, int init);
void callGetTable(IrRegAllocX64& regs, AssemblyBuilderX64& build, int rb, OperandX64 c, int ra); void callGetTable(IrRegAllocX64& regs, AssemblyBuilderX64& build, int rb, OperandX64 c, int ra);
void callSetTable(IrRegAllocX64& regs, AssemblyBuilderX64& build, int rb, OperandX64 c, int ra); void callSetTable(IrRegAllocX64& regs, AssemblyBuilderX64& build, int rb, OperandX64 c, int ra);
void checkObjectBarrierConditions(AssemblyBuilderX64& build, RegisterX64 tmp, RegisterX64 object, int ra, int ratag, Label& skip); void checkObjectBarrierConditions(AssemblyBuilderX64& build, RegisterX64 tmp, RegisterX64 object, int ra, int ratag, Label& skip);

View File

@ -251,7 +251,7 @@ void emitInstReturn(AssemblyBuilderX64& build, ModuleHelpers& helpers, int ra, i
} }
} }
void emitInstSetList(IrRegAllocX64& regs, AssemblyBuilderX64& build, int ra, int rb, int count, uint32_t index) void emitInstSetList(IrRegAllocX64& regs, AssemblyBuilderX64& build, int ra, int rb, int count, uint32_t index, int knownSize)
{ {
// TODO: This should use IrCallWrapperX64 // TODO: This should use IrCallWrapperX64
RegisterX64 rArg1 = (build.abi == ABIX64::Windows) ? rcx : rdi; RegisterX64 rArg1 = (build.abi == ABIX64::Windows) ? rcx : rdi;
@ -285,12 +285,14 @@ void emitInstSetList(IrRegAllocX64& regs, AssemblyBuilderX64& build, int ra, int
build.add(last, index - 1); build.add(last, index - 1);
} }
Label skipResize;
RegisterX64 table = regs.takeReg(rax, kInvalidInstIdx); RegisterX64 table = regs.takeReg(rax, kInvalidInstIdx);
build.mov(table, luauRegValue(ra)); build.mov(table, luauRegValue(ra));
if (count == LUA_MULTRET || knownSize < 0 || knownSize < int(index + count - 1))
{
Label skipResize;
// Resize if h->sizearray < last // Resize if h->sizearray < last
build.cmp(dword[table + offsetof(Table, sizearray)], last); build.cmp(dword[table + offsetof(Table, sizearray)], last);
build.jcc(ConditionX64::NotBelow, skipResize); build.jcc(ConditionX64::NotBelow, skipResize);
@ -301,9 +303,10 @@ void emitInstSetList(IrRegAllocX64& regs, AssemblyBuilderX64& build, int ra, int
build.mov(rArg2, table); build.mov(rArg2, table);
build.mov(rArg1, rState); build.mov(rArg1, rState);
build.call(qword[rNativeContext + offsetof(NativeContext, luaH_resizearray)]); build.call(qword[rNativeContext + offsetof(NativeContext, luaH_resizearray)]);
build.mov(table, luauRegValue(ra)); // Reload cloberred register value build.mov(table, luauRegValue(ra)); // Reload clobbered register value
build.setLabel(skipResize); build.setLabel(skipResize);
}
RegisterX64 arrayDst = rdx; RegisterX64 arrayDst = rdx;
RegisterX64 offset = rcx; RegisterX64 offset = rcx;

View File

@ -19,7 +19,7 @@ struct IrRegAllocX64;
void emitInstCall(AssemblyBuilderX64& build, ModuleHelpers& helpers, int ra, int nparams, int nresults); void emitInstCall(AssemblyBuilderX64& build, ModuleHelpers& helpers, int ra, int nparams, int nresults);
void emitInstReturn(AssemblyBuilderX64& build, ModuleHelpers& helpers, int ra, int actualResults, bool functionVariadic); void emitInstReturn(AssemblyBuilderX64& build, ModuleHelpers& helpers, int ra, int actualResults, bool functionVariadic);
void emitInstSetList(IrRegAllocX64& regs, AssemblyBuilderX64& build, int ra, int rb, int count, uint32_t index); void emitInstSetList(IrRegAllocX64& regs, AssemblyBuilderX64& build, int ra, int rb, int count, uint32_t index, int knownSize);
void emitInstForGLoop(AssemblyBuilderX64& build, int ra, int aux, Label& loopRepeat); void emitInstForGLoop(AssemblyBuilderX64& build, int ra, int aux, Label& loopRepeat);
} // namespace X64 } // namespace X64

View File

@ -253,15 +253,6 @@ static void visitVmRegDefsUses(T& visitor, IrFunction& function, const IrBlock&
case IrCmd::SET_UPVALUE: case IrCmd::SET_UPVALUE:
visitor.use(inst.b); visitor.use(inst.b);
break; break;
case IrCmd::PREPARE_FORN:
visitor.use(inst.a);
visitor.use(inst.b);
visitor.use(inst.c);
visitor.def(inst.a);
visitor.def(inst.b);
visitor.def(inst.c);
break;
case IrCmd::INTERRUPT: case IrCmd::INTERRUPT:
break; break;
case IrCmd::BARRIER_OBJ: case IrCmd::BARRIER_OBJ:

View File

@ -373,7 +373,7 @@ void IrBuilder::translateInst(LuauOpcode op, const Instruction* pc, int i)
translateInstDupTable(*this, pc, i); translateInstDupTable(*this, pc, i);
break; break;
case LOP_SETLIST: case LOP_SETLIST:
inst(IrCmd::SETLIST, constUint(i), vmReg(LUAU_INSN_A(*pc)), vmReg(LUAU_INSN_B(*pc)), constInt(LUAU_INSN_C(*pc) - 1), constUint(pc[1])); inst(IrCmd::SETLIST, constUint(i), vmReg(LUAU_INSN_A(*pc)), vmReg(LUAU_INSN_B(*pc)), constInt(LUAU_INSN_C(*pc) - 1), constUint(pc[1]), undef());
break; break;
case LOP_GETUPVAL: case LOP_GETUPVAL:
translateInstGetUpval(*this, pc, i); translateInstGetUpval(*this, pc, i);

View File

@ -13,7 +13,7 @@ namespace CodeGen
namespace X64 namespace X64
{ {
static const std::array<OperandX64, 6> kWindowsGprOrder = {rcx, rdx, r8, r9, addr[rsp + 32], addr[rsp + 40]}; static const std::array<OperandX64, 6> kWindowsGprOrder = {rcx, rdx, r8, r9, addr[rsp + kStackRegHomeStorage], addr[rsp + kStackRegHomeStorage + 8]};
static const std::array<OperandX64, 6> kSystemvGprOrder = {rdi, rsi, rdx, rcx, r8, r9}; static const std::array<OperandX64, 6> kSystemvGprOrder = {rdi, rsi, rdx, rcx, r8, r9};
static const std::array<OperandX64, 4> kXmmOrder = {xmm0, xmm1, xmm2, xmm3}; // Common order for first 4 fp arguments on Windows/SystemV static const std::array<OperandX64, 4> kXmmOrder = {xmm0, xmm1, xmm2, xmm3}; // Common order for first 4 fp arguments on Windows/SystemV

View File

@ -213,8 +213,6 @@ const char* getCmdName(IrCmd cmd)
return "GET_UPVALUE"; return "GET_UPVALUE";
case IrCmd::SET_UPVALUE: case IrCmd::SET_UPVALUE:
return "SET_UPVALUE"; return "SET_UPVALUE";
case IrCmd::PREPARE_FORN:
return "PREPARE_FORN";
case IrCmd::CHECK_TAG: case IrCmd::CHECK_TAG:
return "CHECK_TAG"; return "CHECK_TAG";
case IrCmd::CHECK_TRUTHY: case IrCmd::CHECK_TRUTHY:

View File

@ -1080,16 +1080,6 @@ void IrLoweringA64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next)
} }
break; break;
} }
case IrCmd::PREPARE_FORN:
regs.spill(build, index);
build.mov(x0, rState);
build.add(x1, rBase, uint16_t(vmRegOp(inst.a) * sizeof(TValue)));
build.add(x2, rBase, uint16_t(vmRegOp(inst.b) * sizeof(TValue)));
build.add(x3, rBase, uint16_t(vmRegOp(inst.c) * sizeof(TValue)));
build.ldr(x4, mem(rNativeContext, offsetof(NativeContext, luaV_prepareFORN)));
build.blr(x4);
// note: no emitUpdateBase necessary because prepareFORN does not reallocate stack
break;
case IrCmd::CHECK_TAG: case IrCmd::CHECK_TAG:
{ {
Label fresh; // used when guard aborts execution or jumps to a VM exit Label fresh; // used when guard aborts execution or jumps to a VM exit

View File

@ -622,6 +622,9 @@ void IrLoweringX64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next)
LUAU_ASSERT(inst.b.kind == IrOpKind::Inst || inst.b.kind == IrOpKind::Constant); LUAU_ASSERT(inst.b.kind == IrOpKind::Inst || inst.b.kind == IrOpKind::Constant);
OperandX64 opb = inst.b.kind == IrOpKind::Inst ? regOp(inst.b) : OperandX64(tagOp(inst.b)); OperandX64 opb = inst.b.kind == IrOpKind::Inst ? regOp(inst.b) : OperandX64(tagOp(inst.b));
if (inst.a.kind == IrOpKind::Constant)
build.cmp(opb, tagOp(inst.a));
else
build.cmp(memRegTagOp(inst.a), opb); build.cmp(memRegTagOp(inst.a), opb);
if (isFallthroughBlock(blockOp(inst.d), next)) if (isFallthroughBlock(blockOp(inst.d), next))
@ -997,9 +1000,6 @@ void IrLoweringX64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next)
callBarrierObject(regs, build, tmp2.release(), {}, vmRegOp(inst.b), inst.c.kind == IrOpKind::Undef ? -1 : tagOp(inst.c)); callBarrierObject(regs, build, tmp2.release(), {}, vmRegOp(inst.b), inst.c.kind == IrOpKind::Undef ? -1 : tagOp(inst.c));
break; break;
} }
case IrCmd::PREPARE_FORN:
callPrepareForN(regs, build, vmRegOp(inst.a), vmRegOp(inst.b), vmRegOp(inst.c));
break;
case IrCmd::CHECK_TAG: case IrCmd::CHECK_TAG:
build.cmp(memRegTagOp(inst.a), tagOp(inst.b)); build.cmp(memRegTagOp(inst.a), tagOp(inst.b));
jumpOrAbortOnUndef(ConditionX64::NotEqual, inst.c, next); jumpOrAbortOnUndef(ConditionX64::NotEqual, inst.c, next);
@ -1205,7 +1205,8 @@ void IrLoweringX64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next)
// Fallbacks to non-IR instruction implementations // Fallbacks to non-IR instruction implementations
case IrCmd::SETLIST: case IrCmd::SETLIST:
regs.assertAllFree(); regs.assertAllFree();
emitInstSetList(regs, build, vmRegOp(inst.b), vmRegOp(inst.c), intOp(inst.d), uintOp(inst.e)); emitInstSetList(
regs, build, vmRegOp(inst.b), vmRegOp(inst.c), intOp(inst.d), uintOp(inst.e), inst.f.kind == IrOpKind::Undef ? -1 : int(uintOp(inst.f)));
break; break;
case IrCmd::CALL: case IrCmd::CALL:
regs.assertAllFree(); regs.assertAllFree();

View File

@ -17,6 +17,7 @@ static const RegisterX64 kGprAllocOrder[] = {rax, rdx, rcx, rbx, rsi, rdi, r8, r
IrRegAllocX64::IrRegAllocX64(AssemblyBuilderX64& build, IrFunction& function) IrRegAllocX64::IrRegAllocX64(AssemblyBuilderX64& build, IrFunction& function)
: build(build) : build(build)
, function(function) , function(function)
, usableXmmRegCount(getXmmRegisterCount(build.abi))
{ {
freeGprMap.fill(true); freeGprMap.fill(true);
gprInstUsers.fill(kInvalidInstIdx); gprInstUsers.fill(kInvalidInstIdx);
@ -28,7 +29,7 @@ RegisterX64 IrRegAllocX64::allocReg(SizeX64 size, uint32_t instIdx)
{ {
if (size == SizeX64::xmmword) if (size == SizeX64::xmmword)
{ {
for (size_t i = 0; i < freeXmmMap.size(); ++i) for (size_t i = 0; i < usableXmmRegCount; ++i)
{ {
if (freeXmmMap[i]) if (freeXmmMap[i])
{ {

View File

@ -609,22 +609,19 @@ 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));
IrOp fallback = build.block(IrBlockKind::Fallback);
IrOp nextStep = build.block(IrBlockKind::Internal);
IrOp direct = build.block(IrBlockKind::Internal); IrOp direct = build.block(IrBlockKind::Internal);
IrOp reverse = build.block(IrBlockKind::Internal); IrOp reverse = build.block(IrBlockKind::Internal);
// When loop parameters are not numbers, VM tries to perform type coercion from string and raises an exception if that fails
// Performing that fallback in native code increases code size and complicates CFG, obscuring the values when they are constant
// To avoid that overhead for an extreemely rare case (that doesn't even typecheck), we exit to VM to handle it
IrOp tagLimit = build.inst(IrCmd::LOAD_TAG, build.vmReg(ra + 0)); IrOp tagLimit = build.inst(IrCmd::LOAD_TAG, build.vmReg(ra + 0));
build.inst(IrCmd::CHECK_TAG, tagLimit, build.constTag(LUA_TNUMBER), fallback); build.inst(IrCmd::CHECK_TAG, tagLimit, build.constTag(LUA_TNUMBER), build.vmExit(pcpos));
IrOp tagStep = build.inst(IrCmd::LOAD_TAG, build.vmReg(ra + 1)); IrOp tagStep = build.inst(IrCmd::LOAD_TAG, build.vmReg(ra + 1));
build.inst(IrCmd::CHECK_TAG, tagStep, build.constTag(LUA_TNUMBER), fallback); build.inst(IrCmd::CHECK_TAG, tagStep, build.constTag(LUA_TNUMBER), build.vmExit(pcpos));
IrOp tagIdx = build.inst(IrCmd::LOAD_TAG, build.vmReg(ra + 2)); IrOp tagIdx = build.inst(IrCmd::LOAD_TAG, build.vmReg(ra + 2));
build.inst(IrCmd::CHECK_TAG, tagIdx, build.constTag(LUA_TNUMBER), fallback); build.inst(IrCmd::CHECK_TAG, tagIdx, build.constTag(LUA_TNUMBER), build.vmExit(pcpos));
build.inst(IrCmd::JUMP, nextStep);
// After successful conversion of arguments to number in a fallback, we return here
build.beginBlock(nextStep);
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));
@ -644,12 +641,6 @@ void translateInstForNPrep(IrBuilder& build, const Instruction* pc, int pcpos)
build.beginBlock(reverse); build.beginBlock(reverse);
build.inst(IrCmd::JUMP_CMP_NUM, limit, idx, build.cond(IrCondition::LessEqual), loopStart, loopExit); build.inst(IrCmd::JUMP_CMP_NUM, limit, idx, build.cond(IrCondition::LessEqual), loopStart, loopExit);
// Fallback will try to convert loop variables to numbers or throw an error
build.beginBlock(fallback);
build.inst(IrCmd::SET_SAVEDPC, build.constUint(pcpos + 1));
build.inst(IrCmd::PREPARE_FORN, build.vmReg(ra + 0), build.vmReg(ra + 1), build.vmReg(ra + 2));
build.inst(IrCmd::JUMP, nextStep);
// Fallthrough in original bytecode is implicit, so we start next internal block here // Fallthrough in original bytecode is implicit, so we start next internal block here
if (build.isInternalBlock(loopStart)) if (build.isInternalBlock(loopStart))
build.beginBlock(loopStart); build.beginBlock(loopStart);

View File

@ -111,7 +111,6 @@ IrValueKind getCmdValueKind(IrCmd cmd)
case IrCmd::CONCAT: case IrCmd::CONCAT:
case IrCmd::GET_UPVALUE: case IrCmd::GET_UPVALUE:
case IrCmd::SET_UPVALUE: case IrCmd::SET_UPVALUE:
case IrCmd::PREPARE_FORN:
case IrCmd::CHECK_TAG: case IrCmd::CHECK_TAG:
case IrCmd::CHECK_TRUTHY: case IrCmd::CHECK_TRUTHY:
case IrCmd::CHECK_READONLY: case IrCmd::CHECK_READONLY:

View File

@ -54,11 +54,6 @@ void IrValueLocationTracking::beforeInstLowering(IrInst& inst)
case IrCmd::GET_UPVALUE: case IrCmd::GET_UPVALUE:
invalidateRestoreOp(inst.a); invalidateRestoreOp(inst.a);
break; break;
case IrCmd::PREPARE_FORN:
invalidateRestoreOp(inst.a);
invalidateRestoreOp(inst.b);
invalidateRestoreOp(inst.c);
break;
case IrCmd::CALL: case IrCmd::CALL:
// Even if result count is limited, all registers starting from function (ra) might be modified // Even if result count is limited, all registers starting from function (ra) might be modified
invalidateRestoreVmRegs(vmRegOp(inst.a), -1); invalidateRestoreVmRegs(vmRegOp(inst.a), -1);

View File

@ -44,7 +44,6 @@ void initFunctions(NativeState& data)
data.context.luaV_equalval = luaV_equalval; data.context.luaV_equalval = luaV_equalval;
data.context.luaV_doarith = luaV_doarith; data.context.luaV_doarith = luaV_doarith;
data.context.luaV_dolen = luaV_dolen; data.context.luaV_dolen = luaV_dolen;
data.context.luaV_prepareFORN = luaV_prepareFORN;
data.context.luaV_gettable = luaV_gettable; data.context.luaV_gettable = luaV_gettable;
data.context.luaV_settable = luaV_settable; data.context.luaV_settable = luaV_settable;
data.context.luaV_getimport = luaV_getimport; data.context.luaV_getimport = luaV_getimport;

View File

@ -35,7 +35,6 @@ struct NativeContext
int (*luaV_equalval)(lua_State* L, const TValue* t1, const TValue* t2) = nullptr; int (*luaV_equalval)(lua_State* L, const TValue* t1, const TValue* t2) = nullptr;
void (*luaV_doarith)(lua_State* L, StkId ra, const TValue* rb, const TValue* rc, TMS op) = nullptr; void (*luaV_doarith)(lua_State* L, StkId ra, const TValue* rb, const TValue* rc, TMS op) = nullptr;
void (*luaV_dolen)(lua_State* L, StkId ra, const TValue* rb) = nullptr; void (*luaV_dolen)(lua_State* L, StkId ra, const TValue* rb) = nullptr;
void (*luaV_prepareFORN)(lua_State* L, StkId plimit, StkId pstep, StkId pinit) = nullptr;
void (*luaV_gettable)(lua_State* L, const TValue* t, TValue* key, StkId val) = nullptr; void (*luaV_gettable)(lua_State* L, const TValue* t, TValue* key, StkId val) = nullptr;
void (*luaV_settable)(lua_State* L, const TValue* t, TValue* key, StkId val) = nullptr; void (*luaV_settable)(lua_State* L, const TValue* t, TValue* key, StkId val) = nullptr;
void (*luaV_getimport)(lua_State* L, Table* env, TValue* k, StkId res, uint32_t id, bool propagatenil) = nullptr; void (*luaV_getimport)(lua_State* L, Table* env, TValue* k, StkId res, uint32_t id, bool propagatenil) = nullptr;

View File

@ -992,25 +992,20 @@ static void constPropInInst(ConstPropState& state, IrBuilder& build, IrFunction&
state.invalidateRegisterRange(vmRegOp(inst.a), function.uintOp(inst.b)); state.invalidateRegisterRange(vmRegOp(inst.a), function.uintOp(inst.b));
state.invalidateUserCall(); // TODO: if only strings and numbers are concatenated, there will be no user calls state.invalidateUserCall(); // TODO: if only strings and numbers are concatenated, there will be no user calls
break; break;
case IrCmd::PREPARE_FORN:
state.invalidateValue(inst.a);
state.saveTag(inst.a, LUA_TNUMBER);
state.invalidateValue(inst.b);
state.saveTag(inst.b, LUA_TNUMBER);
state.invalidateValue(inst.c);
state.saveTag(inst.c, LUA_TNUMBER);
break;
case IrCmd::INTERRUPT: case IrCmd::INTERRUPT:
state.invalidateUserCall(); state.invalidateUserCall();
break; break;
case IrCmd::SETLIST: case IrCmd::SETLIST:
if (RegisterInfo* info = state.tryGetRegisterInfo(inst.b); info && info->knownTableArraySize >= 0)
replace(function, inst.f, build.constUint(info->knownTableArraySize));
state.valueMap.clear(); // TODO: this can be relaxed when x64 emitInstSetList becomes aware of register allocator state.valueMap.clear(); // TODO: this can be relaxed when x64 emitInstSetList becomes aware of register allocator
break; break;
case IrCmd::CALL: case IrCmd::CALL:
state.invalidateRegistersFrom(vmRegOp(inst.a)); state.invalidateRegistersFrom(vmRegOp(inst.a));
state.invalidateUserCall(); state.invalidateUserCall();
// We cannot guarantee right now that all live values can be remeterialized from non-stack memory locations // We cannot guarantee right now that all live values can be rematerialized from non-stack memory locations
// To prevent earlier values from being propagated to after the call, we have to clear the map // To prevent earlier values from being propagated to after the call, we have to clear the map
// TODO: remove only the values that don't have a guaranteed restore location // TODO: remove only the values that don't have a guaranteed restore location
state.valueMap.clear(); state.valueMap.clear();

View File

@ -225,9 +225,10 @@ void UnwindBuilderDwarf2::prologueA64(uint32_t prologueSize, uint32_t stackSize,
} }
} }
void UnwindBuilderDwarf2::prologueX64(uint32_t prologueSize, uint32_t stackSize, bool setupFrame, std::initializer_list<X64::RegisterX64> regs) void UnwindBuilderDwarf2::prologueX64(uint32_t prologueSize, uint32_t stackSize, bool setupFrame, std::initializer_list<X64::RegisterX64> gpr,
const std::vector<X64::RegisterX64>& simd)
{ {
LUAU_ASSERT(stackSize > 0 && stackSize <= 128 && stackSize % 8 == 0); LUAU_ASSERT(stackSize > 0 && stackSize < 4096 && stackSize % 8 == 0);
unsigned int stackOffset = 8; // Return address was pushed by calling the function unsigned int stackOffset = 8; // Return address was pushed by calling the function
unsigned int prologueOffset = 0; unsigned int prologueOffset = 0;
@ -247,7 +248,7 @@ void UnwindBuilderDwarf2::prologueX64(uint32_t prologueSize, uint32_t stackSize,
} }
// push reg // push reg
for (X64::RegisterX64 reg : regs) for (X64::RegisterX64 reg : gpr)
{ {
LUAU_ASSERT(reg.size == X64::SizeX64::qword); LUAU_ASSERT(reg.size == X64::SizeX64::qword);
@ -258,9 +259,11 @@ void UnwindBuilderDwarf2::prologueX64(uint32_t prologueSize, uint32_t stackSize,
pos = defineSavedRegisterLocation(pos, regIndexToDwRegX64[reg.index], stackOffset); pos = defineSavedRegisterLocation(pos, regIndexToDwRegX64[reg.index], stackOffset);
} }
LUAU_ASSERT(simd.empty());
// sub rsp, stackSize // sub rsp, stackSize
stackOffset += stackSize; stackOffset += stackSize;
prologueOffset += 4; prologueOffset += stackSize >= 128 ? 7 : 4;
pos = advanceLocation(pos, 4); pos = advanceLocation(pos, 4);
pos = defineCfaExpressionOffset(pos, stackOffset); pos = defineCfaExpressionOffset(pos, stackOffset);

View File

@ -82,7 +82,7 @@ void UnwindBuilderWin::finishFunction(uint32_t beginOffset, uint32_t endOffset)
if (!unwindCodes.empty()) if (!unwindCodes.empty())
{ {
// Copy unwind codes in reverse order // Copy unwind codes in reverse order
// Some unwind codes take up two array slots, but we don't use those atm // Some unwind codes take up two array slots, we write those in reverse order
uint8_t* unwindCodePos = rawDataPos + sizeof(UnwindCodeWin) * (unwindCodes.size() - 1); uint8_t* unwindCodePos = rawDataPos + sizeof(UnwindCodeWin) * (unwindCodes.size() - 1);
LUAU_ASSERT(unwindCodePos <= rawData + kRawDataLimit); LUAU_ASSERT(unwindCodePos <= rawData + kRawDataLimit);
@ -109,9 +109,10 @@ void UnwindBuilderWin::prologueA64(uint32_t prologueSize, uint32_t stackSize, st
LUAU_ASSERT(!"Not implemented"); LUAU_ASSERT(!"Not implemented");
} }
void UnwindBuilderWin::prologueX64(uint32_t prologueSize, uint32_t stackSize, bool setupFrame, std::initializer_list<X64::RegisterX64> regs) void UnwindBuilderWin::prologueX64(uint32_t prologueSize, uint32_t stackSize, bool setupFrame, std::initializer_list<X64::RegisterX64> gpr,
const std::vector<X64::RegisterX64>& simd)
{ {
LUAU_ASSERT(stackSize > 0 && stackSize <= 128 && stackSize % 8 == 0); LUAU_ASSERT(stackSize > 0 && stackSize < 4096 && stackSize % 8 == 0);
LUAU_ASSERT(prologueSize < 256); LUAU_ASSERT(prologueSize < 256);
unsigned int stackOffset = 8; // Return address was pushed by calling the function unsigned int stackOffset = 8; // Return address was pushed by calling the function
@ -132,7 +133,7 @@ void UnwindBuilderWin::prologueX64(uint32_t prologueSize, uint32_t stackSize, bo
} }
// push reg // push reg
for (X64::RegisterX64 reg : regs) for (X64::RegisterX64 reg : gpr)
{ {
LUAU_ASSERT(reg.size == X64::SizeX64::qword); LUAU_ASSERT(reg.size == X64::SizeX64::qword);
@ -141,10 +142,51 @@ void UnwindBuilderWin::prologueX64(uint32_t prologueSize, uint32_t stackSize, bo
unwindCodes.push_back({uint8_t(prologueOffset), UWOP_PUSH_NONVOL, reg.index}); unwindCodes.push_back({uint8_t(prologueOffset), UWOP_PUSH_NONVOL, reg.index});
} }
// If frame pointer is used, simd register storage is not implemented, it will require reworking store offsets
LUAU_ASSERT(!setupFrame || simd.size() == 0);
unsigned int simdStorageSize = unsigned(simd.size()) * 16;
// It's the responsibility of the caller to provide simd register storage in 'stackSize', including alignment to 16 bytes
if (!simd.empty() && stackOffset % 16 == 8)
simdStorageSize += 8;
// sub rsp, stackSize // sub rsp, stackSize
if (stackSize <= 128)
{
stackOffset += stackSize; stackOffset += stackSize;
prologueOffset += 4; prologueOffset += stackSize == 128 ? 7 : 4;
unwindCodes.push_back({uint8_t(prologueOffset), UWOP_ALLOC_SMALL, uint8_t((stackSize - 8) / 8)}); unwindCodes.push_back({uint8_t(prologueOffset), UWOP_ALLOC_SMALL, uint8_t((stackSize - 8) / 8)});
}
else
{
// This command can handle allocations up to 512K-8 bytes, but that potentially requires stack probing
LUAU_ASSERT(stackSize < 4096);
stackOffset += stackSize;
prologueOffset += 7;
uint16_t encodedOffset = stackSize / 8;
unwindCodes.push_back(UnwindCodeWin());
memcpy(&unwindCodes.back(), &encodedOffset, sizeof(encodedOffset));
unwindCodes.push_back({uint8_t(prologueOffset), UWOP_ALLOC_LARGE, 0});
}
// It's the responsibility of the caller to provide simd register storage in 'stackSize'
unsigned int xmmStoreOffset = stackSize - simdStorageSize;
// vmovaps [rsp+n], xmm
for (X64::RegisterX64 reg : simd)
{
LUAU_ASSERT(reg.size == X64::SizeX64::xmmword);
LUAU_ASSERT(xmmStoreOffset % 16 == 0 && "simd stores have to be performed to aligned locations");
prologueOffset += xmmStoreOffset >= 128 ? 10 : 7;
unwindCodes.push_back({uint8_t(xmmStoreOffset / 16), 0, 0});
unwindCodes.push_back({uint8_t(prologueOffset), UWOP_SAVE_XMM128, reg.index});
xmmStoreOffset += 16;
}
LUAU_ASSERT(stackOffset % 16 == 0); LUAU_ASSERT(stackOffset % 16 == 0);
LUAU_ASSERT(prologueOffset == prologueSize); LUAU_ASSERT(prologueOffset == prologueSize);

View File

@ -282,6 +282,13 @@ public:
class const_iterator class const_iterator
{ {
public: public:
using value_type = Item;
using reference = Item&;
using pointer = Item*;
using iterator = pointer;
using difference_type = size_t;
using iterator_category = std::input_iterator_tag;
const_iterator() const_iterator()
: set(0) : set(0)
, index(0) , index(0)

View File

@ -4,9 +4,6 @@
#include "Luau/Bytecode.h" #include "Luau/Bytecode.h"
#include "Luau/Compiler.h" #include "Luau/Compiler.h"
LUAU_FASTFLAGVARIABLE(LuauCompileBuiltinTonumber, false)
LUAU_FASTFLAGVARIABLE(LuauCompileBuiltinTostring, false)
namespace Luau namespace Luau
{ {
namespace Compile namespace Compile
@ -72,9 +69,9 @@ static int getBuiltinFunctionId(const Builtin& builtin, const CompileOptions& op
if (builtin.isGlobal("setmetatable")) if (builtin.isGlobal("setmetatable"))
return LBF_SETMETATABLE; return LBF_SETMETATABLE;
if (FFlag::LuauCompileBuiltinTonumber && builtin.isGlobal("tonumber")) if (builtin.isGlobal("tonumber"))
return LBF_TONUMBER; return LBF_TONUMBER;
if (FFlag::LuauCompileBuiltinTostring && builtin.isGlobal("tostring")) if (builtin.isGlobal("tostring"))
return LBF_TOSTRING; return LBF_TOSTRING;
if (builtin.object == "math") if (builtin.object == "math")

View File

@ -26,8 +26,6 @@ LUAU_FASTINTVARIABLE(LuauCompileInlineThreshold, 25)
LUAU_FASTINTVARIABLE(LuauCompileInlineThresholdMaxBoost, 300) LUAU_FASTINTVARIABLE(LuauCompileInlineThresholdMaxBoost, 300)
LUAU_FASTINTVARIABLE(LuauCompileInlineDepth, 5) LUAU_FASTINTVARIABLE(LuauCompileInlineDepth, 5)
LUAU_FASTFLAGVARIABLE(LuauCompileFoldMathK, false)
namespace Luau namespace Luau
{ {
@ -3871,7 +3869,6 @@ void compileOrThrow(BytecodeBuilder& bytecode, const ParseResult& parseResult, c
{ {
compiler.builtinsFold = &compiler.builtins; compiler.builtinsFold = &compiler.builtins;
if (FFlag::LuauCompileFoldMathK)
if (AstName math = names.get("math"); math.value && getGlobalState(compiler.globals, math) == Global::Default) if (AstName math = names.get("math"); math.value && getGlobalState(compiler.globals, math) == Global::Default)
compiler.builtinsFoldMathK = true; compiler.builtinsFoldMathK = true;
} }

View File

@ -121,12 +121,11 @@ ifeq ($(protobuf),download)
endif endif
ifneq ($(native),) ifneq ($(native),)
CXXFLAGS+=-DLUA_CUSTOM_EXECUTION=1
TESTS_ARGS+=--codegen TESTS_ARGS+=--codegen
endif endif
ifneq ($(nativelj),) ifneq ($(nativelj),)
CXXFLAGS+=-DLUA_CUSTOM_EXECUTION=1 -DLUA_USE_LONGJMP=1 CXXFLAGS+=-DLUA_USE_LONGJMP=1
TESTS_ARGS+=--codegen TESTS_ARGS+=--codegen
endif endif
@ -142,7 +141,7 @@ $(TESTS_OBJECTS): CXXFLAGS+=-std=c++17 -ICommon/include -IAst/include -ICompiler
$(REPL_CLI_OBJECTS): CXXFLAGS+=-std=c++17 -ICommon/include -IAst/include -ICompiler/include -IVM/include -ICodeGen/include -Iextern -Iextern/isocline/include $(REPL_CLI_OBJECTS): CXXFLAGS+=-std=c++17 -ICommon/include -IAst/include -ICompiler/include -IVM/include -ICodeGen/include -Iextern -Iextern/isocline/include
$(ANALYZE_CLI_OBJECTS): CXXFLAGS+=-std=c++17 -ICommon/include -IAst/include -IAnalysis/include -IConfig/include -Iextern $(ANALYZE_CLI_OBJECTS): CXXFLAGS+=-std=c++17 -ICommon/include -IAst/include -IAnalysis/include -IConfig/include -Iextern
$(COMPILE_CLI_OBJECTS): CXXFLAGS+=-std=c++17 -ICommon/include -IAst/include -ICompiler/include -IVM/include -ICodeGen/include $(COMPILE_CLI_OBJECTS): CXXFLAGS+=-std=c++17 -ICommon/include -IAst/include -ICompiler/include -IVM/include -ICodeGen/include
$(FUZZ_OBJECTS): CXXFLAGS+=-std=c++17 -ICommon/include -IAst/include -ICompiler/include -IAnalysis/include -IVM/include -ICodeGen/include $(FUZZ_OBJECTS): CXXFLAGS+=-std=c++17 -ICommon/include -IAst/include -ICompiler/include -IAnalysis/include -IVM/include -ICodeGen/include -IConfig/include
$(TESTS_TARGET): LDFLAGS+=-lpthread $(TESTS_TARGET): LDFLAGS+=-lpthread
$(REPL_CLI_TARGET): LDFLAGS+=-lpthread $(REPL_CLI_TARGET): LDFLAGS+=-lpthread
@ -219,11 +218,11 @@ $(TESTS_TARGET) $(REPL_CLI_TARGET) $(ANALYZE_CLI_TARGET) $(COMPILE_CLI_TARGET):
$(CXX) $^ $(LDFLAGS) -o $@ $(CXX) $^ $(LDFLAGS) -o $@
# executable targets for fuzzing # executable targets for fuzzing
fuzz-%: $(BUILD)/fuzz/%.cpp.o $(ANALYSIS_TARGET) $(COMPILER_TARGET) $(AST_TARGET) $(CODEGEN_TARGET) $(VM_TARGET) fuzz-%: $(BUILD)/fuzz/%.cpp.o $(ANALYSIS_TARGET) $(COMPILER_TARGET) $(AST_TARGET) $(CONFIG_TARGET) $(CODEGEN_TARGET) $(VM_TARGET)
$(CXX) $^ $(LDFLAGS) -o $@ $(CXX) $^ $(LDFLAGS) -o $@
fuzz-proto: $(BUILD)/fuzz/proto.cpp.o $(BUILD)/fuzz/protoprint.cpp.o $(BUILD)/fuzz/luau.pb.cpp.o $(ANALYSIS_TARGET) $(COMPILER_TARGET) $(AST_TARGET) $(VM_TARGET) | build/libprotobuf-mutator fuzz-proto: $(BUILD)/fuzz/proto.cpp.o $(BUILD)/fuzz/protoprint.cpp.o $(BUILD)/fuzz/luau.pb.cpp.o $(ANALYSIS_TARGET) $(COMPILER_TARGET) $(AST_TARGET) $(CONFIG_TARGET) $(VM_TARGET) | build/libprotobuf-mutator
fuzz-prototest: $(BUILD)/fuzz/prototest.cpp.o $(BUILD)/fuzz/protoprint.cpp.o $(BUILD)/fuzz/luau.pb.cpp.o $(ANALYSIS_TARGET) $(COMPILER_TARGET) $(AST_TARGET) $(VM_TARGET) | build/libprotobuf-mutator fuzz-prototest: $(BUILD)/fuzz/prototest.cpp.o $(BUILD)/fuzz/protoprint.cpp.o $(BUILD)/fuzz/luau.pb.cpp.o $(ANALYSIS_TARGET) $(COMPILER_TARGET) $(AST_TARGET) $(CONFIG_TARGET) $(VM_TARGET) | build/libprotobuf-mutator
# static library targets # static library targets
$(AST_TARGET): $(AST_OBJECTS) $(AST_TARGET): $(AST_OBJECTS)

View File

@ -121,11 +121,6 @@
#define LUA_MAXCAPTURES 32 #define LUA_MAXCAPTURES 32
#endif #endif
// enables callbacks to redirect code execution from Luau VM to a custom implementation
#ifndef LUA_CUSTOM_EXECUTION
#define LUA_CUSTOM_EXECUTION 1
#endif
// }================================================================== // }==================================================================
/* /*

View File

@ -11,8 +11,6 @@
#include <string.h> #include <string.h>
LUAU_FASTFLAG(LuauFasterInterp)
// convert a stack index to positive // convert a stack index to positive
#define abs_index(L, i) ((i) > 0 || (i) <= LUA_REGISTRYINDEX ? (i) : lua_gettop(L) + (i) + 1) #define abs_index(L, i) ((i) > 0 || (i) <= LUA_REGISTRYINDEX ? (i) : lua_gettop(L) + (i) + 1)
@ -523,21 +521,12 @@ void luaL_pushresultsize(luaL_Buffer* B, size_t size)
const char* luaL_tolstring(lua_State* L, int idx, size_t* len) const char* luaL_tolstring(lua_State* L, int idx, size_t* len)
{ {
if (luaL_callmeta(L, idx, "__tostring")) // is there a metafield? if (luaL_callmeta(L, idx, "__tostring")) // is there a metafield?
{
if (FFlag::LuauFasterInterp)
{ {
const char* s = lua_tolstring(L, -1, len); const char* s = lua_tolstring(L, -1, len);
if (!s) if (!s)
luaL_error(L, "'__tostring' must return a string"); luaL_error(L, "'__tostring' must return a string");
return s; return s;
} }
else
{
if (!lua_isstring(L, -1))
luaL_error(L, "'__tostring' must return a string");
return lua_tolstring(L, -1, len);
}
}
switch (lua_type(L, idx)) switch (lua_type(L, idx))
{ {

View File

@ -23,8 +23,6 @@
#endif #endif
#endif #endif
LUAU_DYNAMIC_FASTFLAGVARIABLE(LuauFastcallGC, false)
// luauF functions implement FASTCALL instruction that performs a direct execution of some builtin functions from the VM // luauF functions implement FASTCALL instruction that performs a direct execution of some builtin functions from the VM
// The rule of thumb is that FASTCALL functions can not call user code, yield, fail, or reallocate stack. // The rule of thumb is that FASTCALL functions can not call user code, yield, fail, or reallocate stack.
// If types of the arguments mismatch, luauF_* needs to return -1 and the execution will fall back to the usual call path // If types of the arguments mismatch, luauF_* needs to return -1 and the execution will fall back to the usual call path
@ -832,7 +830,7 @@ static int luauF_char(lua_State* L, StkId res, TValue* arg0, int nresults, StkId
if (nparams < int(sizeof(buffer)) && nresults <= 1) if (nparams < int(sizeof(buffer)) && nresults <= 1)
{ {
if (DFFlag::LuauFastcallGC && luaC_needsGC(L)) if (luaC_needsGC(L))
return -1; // we can't call luaC_checkGC so fall back to C implementation return -1; // we can't call luaC_checkGC so fall back to C implementation
if (nparams >= 1) if (nparams >= 1)
@ -904,7 +902,7 @@ static int luauF_sub(lua_State* L, StkId res, TValue* arg0, int nresults, StkId
int i = int(nvalue(args)); int i = int(nvalue(args));
int j = int(nvalue(args + 1)); int j = int(nvalue(args + 1));
if (DFFlag::LuauFastcallGC && luaC_needsGC(L)) if (luaC_needsGC(L))
return -1; // we can't call luaC_checkGC so fall back to C implementation return -1; // we can't call luaC_checkGC so fall back to C implementation
if (i >= 1 && j >= i && unsigned(j - 1) < unsigned(ts->len)) if (i >= 1 && j >= i && unsigned(j - 1) < unsigned(ts->len))
@ -1300,7 +1298,7 @@ static int luauF_tostring(lua_State* L, StkId res, TValue* arg0, int nresults, S
} }
case LUA_TNUMBER: case LUA_TNUMBER:
{ {
if (DFFlag::LuauFastcallGC && luaC_needsGC(L)) if (luaC_needsGC(L))
return -1; // we can't call luaC_checkGC so fall back to C implementation return -1; // we can't call luaC_checkGC so fall back to C implementation
char s[LUAI_MAXNUM2STR]; char s[LUAI_MAXNUM2STR];

View File

@ -8,9 +8,6 @@
#include <string.h> #include <string.h>
#include <stdio.h> #include <stdio.h>
LUAU_FASTFLAGVARIABLE(LuauFasterInterp, false)
LUAU_FASTFLAGVARIABLE(LuauFasterFormatS, false)
// macro to `unsign' a character // macro to `unsign' a character
#define uchar(c) ((unsigned char)(c)) #define uchar(c) ((unsigned char)(c))
@ -969,7 +966,7 @@ static int str_format(lua_State* L)
luaL_addchar(&b, *strfrmt++); luaL_addchar(&b, *strfrmt++);
else if (*++strfrmt == L_ESC) else if (*++strfrmt == L_ESC)
luaL_addchar(&b, *strfrmt++); // %% luaL_addchar(&b, *strfrmt++); // %%
else if (FFlag::LuauFasterInterp && *strfrmt == '*') else if (*strfrmt == '*')
{ {
strfrmt++; strfrmt++;
if (++arg > top) if (++arg > top)
@ -1029,8 +1026,6 @@ static int str_format(lua_State* L)
{ {
size_t l; size_t l;
const char* s = luaL_checklstring(L, arg, &l); const char* s = luaL_checklstring(L, arg, &l);
if (FFlag::LuauFasterFormatS)
{
// no precision and string is too long to be formatted, or no format necessary to begin with // no precision and string is too long to be formatted, or no format necessary to begin with
if (form[2] == '\0' || (!strchr(form, '.') && l >= 100)) if (form[2] == '\0' || (!strchr(form, '.') && l >= 100))
{ {
@ -1043,35 +1038,10 @@ static int str_format(lua_State* L)
break; break;
} }
} }
else
{
if (!strchr(form, '.') && l >= 100)
{
/* no precision and string is too long to be formatted;
keep original string */
lua_pushvalue(L, arg);
luaL_addvalue(&b);
continue; // skip the `luaL_addlstring' at the end
}
else
{
snprintf(buff, sizeof(buff), form, s);
break;
}
}
}
case '*': case '*':
{ {
if (FFlag::LuauFasterInterp || formatItemSize != 1) // %* is parsed above, so if we got here we must have %...*
luaL_error(L, "'%%*' does not take a form"); luaL_error(L, "'%%*' does not take a form");
size_t length;
const char* string = luaL_tolstring(L, arg, &length);
luaL_addlstring(&b, string, length, -2);
lua_pop(L, 1);
continue; // skip the `luaL_addlstring' at the end
} }
default: default:
{ // also treat cases `pnLlh' { // also treat cases `pnLlh'

View File

@ -132,7 +132,7 @@
#endif #endif
// Does VM support native execution via ExecutionCallbacks? We mostly assume it does but keep the define to make it easy to quantify the cost. // Does VM support native execution via ExecutionCallbacks? We mostly assume it does but keep the define to make it easy to quantify the cost.
#define VM_HAS_NATIVE LUA_CUSTOM_EXECUTION #define VM_HAS_NATIVE 1
LUAU_NOINLINE void luau_callhook(lua_State* L, lua_Hook hook, void* userdata) LUAU_NOINLINE void luau_callhook(lua_State* L, lua_Hook hook, void* userdata)
{ {
@ -2380,7 +2380,7 @@ reentry:
else else
goto exit; goto exit;
#else #else
LUAU_ASSERT(!"Opcode is only valid when LUA_CUSTOM_EXECUTION is defined"); LUAU_ASSERT(!"Opcode is only valid when VM_HAS_NATIVE is defined");
LUAU_UNREACHABLE(); LUAU_UNREACHABLE();
#endif #endif
} }

View File

@ -7,6 +7,7 @@
#include "Luau/CodeGen.h" #include "Luau/CodeGen.h"
#include "Luau/Common.h" #include "Luau/Common.h"
#include "Luau/Compiler.h" #include "Luau/Compiler.h"
#include "Luau/Config.h"
#include "Luau/Frontend.h" #include "Luau/Frontend.h"
#include "Luau/Linter.h" #include "Luau/Linter.h"
#include "Luau/ModuleResolver.h" #include "Luau/ModuleResolver.h"

View File

@ -295,4 +295,34 @@ TEST_CASE_FIXTURE(Fixture, "include_types_ancestry")
CHECK(ancestryTypes.back()->asType()); CHECK(ancestryTypes.back()->asType());
} }
TEST_CASE_FIXTURE(Fixture, "find_name_ancestry")
{
ScopedFastFlag sff{"FixFindBindingAtFunctionName", true};
check(R"(
local tbl = {}
function tbl:abc() end
)");
const Position pos(2, 18);
std::vector<AstNode*> ancestry = findAstAncestryOfPosition(*getMainSourceModule(), pos);
REQUIRE(!ancestry.empty());
CHECK(ancestry.back()->is<AstExprLocal>());
}
TEST_CASE_FIXTURE(Fixture, "find_expr_ancestry")
{
ScopedFastFlag sff{"FixFindBindingAtFunctionName", true};
check(R"(
local tbl = {}
function tbl:abc() end
)");
const Position pos(2, 29);
std::vector<AstNode*> ancestry = findAstAncestryOfPosition(*getMainSourceModule(), pos);
REQUIRE(!ancestry.empty());
CHECK(ancestry.back()->is<AstExprFunction>());
}
TEST_SUITE_END(); TEST_SUITE_END();

View File

@ -80,7 +80,7 @@ struct ACFixtureImpl : BaseType
{ {
if (prevChar == '@') if (prevChar == '@')
{ {
LUAU_ASSERT("Illegal marker character" && c >= '0' && c <= '9'); LUAU_ASSERT("Illegal marker character" && ((c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z')));
LUAU_ASSERT("Duplicate marker found" && markerPosition.count(c) == 0); LUAU_ASSERT("Duplicate marker found" && markerPosition.count(c) == 0);
markerPosition.insert(std::pair{c, curPos}); markerPosition.insert(std::pair{c, curPos});
} }
@ -126,7 +126,6 @@ struct ACFixtureImpl : BaseType
LUAU_ASSERT(i != markerPosition.end()); LUAU_ASSERT(i != markerPosition.end());
return i->second; return i->second;
} }
ScopedFastFlag flag{"LuauAutocompleteHideSelfArg", true};
// Maps a marker character (0-9 inclusive) to a position in the source code. // Maps a marker character (0-9 inclusive) to a position in the source code.
std::map<char, Position> markerPosition; std::map<char, Position> markerPosition;
}; };
@ -3083,6 +3082,86 @@ TEST_CASE_FIXTURE(ACFixture, "string_singleton_as_table_key")
CHECK(ac.entryMap.count("\"down\"")); CHECK(ac.entryMap.count("\"down\""));
} }
// https://github.com/Roblox/luau/issues/858
TEST_CASE_FIXTURE(ACFixture, "string_singleton_in_if_statement")
{
ScopedFastFlag sff{"LuauAutocompleteStringLiteralBounds", true};
check(R"(
--!strict
type Direction = "left" | "right"
local dir: Direction = "left"
if dir == @1"@2"@3 then end
local a: {[Direction]: boolean} = {[@4"@5"@6]}
if dir == @7`@8`@9 then end
local a: {[Direction]: boolean} = {[@A`@B`@C]}
)");
auto ac = autocomplete('1');
CHECK(!ac.entryMap.count("left"));
CHECK(!ac.entryMap.count("right"));
ac = autocomplete('2');
CHECK(ac.entryMap.count("left"));
CHECK(ac.entryMap.count("right"));
ac = autocomplete('3');
CHECK(!ac.entryMap.count("left"));
CHECK(!ac.entryMap.count("right"));
ac = autocomplete('4');
CHECK(!ac.entryMap.count("left"));
CHECK(!ac.entryMap.count("right"));
ac = autocomplete('5');
CHECK(ac.entryMap.count("left"));
CHECK(ac.entryMap.count("right"));
ac = autocomplete('6');
CHECK(!ac.entryMap.count("left"));
CHECK(!ac.entryMap.count("right"));
ac = autocomplete('7');
CHECK(!ac.entryMap.count("left"));
CHECK(!ac.entryMap.count("right"));
ac = autocomplete('8');
CHECK(ac.entryMap.count("left"));
CHECK(ac.entryMap.count("right"));
ac = autocomplete('9');
CHECK(!ac.entryMap.count("left"));
CHECK(!ac.entryMap.count("right"));
ac = autocomplete('A');
CHECK(!ac.entryMap.count("left"));
CHECK(!ac.entryMap.count("right"));
ac = autocomplete('B');
CHECK(ac.entryMap.count("left"));
CHECK(ac.entryMap.count("right"));
ac = autocomplete('C');
CHECK(!ac.entryMap.count("left"));
CHECK(!ac.entryMap.count("right"));
}
TEST_CASE_FIXTURE(ACFixture, "autocomplete_string_singleton_equality") TEST_CASE_FIXTURE(ACFixture, "autocomplete_string_singleton_equality")
{ {
check(R"( check(R"(

View File

@ -187,7 +187,7 @@ TEST_CASE("WindowsUnwindCodesX64")
unwind.startInfo(UnwindBuilder::X64); unwind.startInfo(UnwindBuilder::X64);
unwind.startFunction(); unwind.startFunction();
unwind.prologueX64(/* prologueSize= */ 23, /* stackSize= */ 72, /* setupFrame= */ true, {rdi, rsi, rbx, r12, r13, r14, r15}); unwind.prologueX64(/* prologueSize= */ 23, /* stackSize= */ 72, /* setupFrame= */ true, {rdi, rsi, rbx, r12, r13, r14, r15}, {});
unwind.finishFunction(0x11223344, 0x55443322); unwind.finishFunction(0x11223344, 0x55443322);
unwind.finishInfo(); unwind.finishInfo();
@ -211,7 +211,7 @@ TEST_CASE("Dwarf2UnwindCodesX64")
unwind.startInfo(UnwindBuilder::X64); unwind.startInfo(UnwindBuilder::X64);
unwind.startFunction(); unwind.startFunction();
unwind.prologueX64(/* prologueSize= */ 23, /* stackSize= */ 72, /* setupFrame= */ true, {rdi, rsi, rbx, r12, r13, r14, r15}); unwind.prologueX64(/* prologueSize= */ 23, /* stackSize= */ 72, /* setupFrame= */ true, {rdi, rsi, rbx, r12, r13, r14, r15}, {});
unwind.finishFunction(0, 0); unwind.finishFunction(0, 0);
unwind.finishInfo(); unwind.finishInfo();
@ -309,6 +309,11 @@ static void throwing(int64_t arg)
throw std::runtime_error("testing"); throw std::runtime_error("testing");
} }
static void nonthrowing(int64_t arg)
{
CHECK(arg == 25);
}
TEST_CASE("GeneratedCodeExecutionWithThrowX64") TEST_CASE("GeneratedCodeExecutionWithThrowX64")
{ {
using namespace X64; using namespace X64;
@ -339,7 +344,7 @@ TEST_CASE("GeneratedCodeExecutionWithThrowX64")
uint32_t prologueSize = build.setLabel().location; uint32_t prologueSize = build.setLabel().location;
unwind->prologueX64(prologueSize, stackSize + localsSize, /* setupFrame= */ true, {rNonVol1, rNonVol2}); unwind->prologueX64(prologueSize, stackSize + localsSize, /* setupFrame= */ true, {rNonVol1, rNonVol2}, {});
// Body // Body
build.mov(rNonVol1, rArg1); build.mov(rNonVol1, rArg1);
@ -379,6 +384,8 @@ TEST_CASE("GeneratedCodeExecutionWithThrowX64")
using FunctionType = int64_t(int64_t, void (*)(int64_t)); using FunctionType = int64_t(int64_t, void (*)(int64_t));
FunctionType* f = (FunctionType*)nativeEntry; FunctionType* f = (FunctionType*)nativeEntry;
f(10, nonthrowing);
// To simplify debugging, CHECK_THROWS_WITH_AS is not used here // To simplify debugging, CHECK_THROWS_WITH_AS is not used here
try try
{ {
@ -390,6 +397,121 @@ TEST_CASE("GeneratedCodeExecutionWithThrowX64")
} }
} }
static void obscureThrowCase(int64_t (*f)(int64_t, void (*)(int64_t)))
{
// To simplify debugging, CHECK_THROWS_WITH_AS is not used here
try
{
f(10, throwing);
}
catch (const std::runtime_error& error)
{
CHECK(strcmp(error.what(), "testing") == 0);
}
}
TEST_CASE("GeneratedCodeExecutionWithThrowX64Simd")
{
// This test requires AVX
if (!Luau::CodeGen::isSupported())
return;
using namespace X64;
AssemblyBuilderX64 build(/* logText= */ false);
#if defined(_WIN32)
std::unique_ptr<UnwindBuilder> unwind = std::make_unique<UnwindBuilderWin>();
#else
std::unique_ptr<UnwindBuilder> unwind = std::make_unique<UnwindBuilderDwarf2>();
#endif
unwind->startInfo(UnwindBuilder::X64);
Label functionBegin = build.setLabel();
unwind->startFunction();
int stackSize = 32 + 64;
int localsSize = 16;
// Prologue
build.push(rNonVol1);
build.push(rNonVol2);
build.push(rbp);
build.sub(rsp, stackSize + localsSize);
if (build.abi == ABIX64::Windows)
{
build.vmovaps(xmmword[rsp + ((stackSize + localsSize) - 0x40)], xmm6);
build.vmovaps(xmmword[rsp + ((stackSize + localsSize) - 0x30)], xmm7);
build.vmovaps(xmmword[rsp + ((stackSize + localsSize) - 0x20)], xmm8);
build.vmovaps(xmmword[rsp + ((stackSize + localsSize) - 0x10)], xmm9);
}
uint32_t prologueSize = build.setLabel().location;
if (build.abi == ABIX64::Windows)
unwind->prologueX64(prologueSize, stackSize + localsSize, /* setupFrame= */ false, {rNonVol1, rNonVol2, rbp}, {xmm6, xmm7, xmm8, xmm9});
else
unwind->prologueX64(prologueSize, stackSize + localsSize, /* setupFrame= */ false, {rNonVol1, rNonVol2, rbp}, {});
// Body
build.vxorpd(xmm0, xmm0, xmm0);
build.vmovsd(xmm6, xmm0, xmm0);
build.vmovsd(xmm7, xmm0, xmm0);
build.vmovsd(xmm8, xmm0, xmm0);
build.vmovsd(xmm9, xmm0, xmm0);
build.mov(rNonVol1, rArg1);
build.mov(rNonVol2, rArg2);
build.add(rNonVol1, 15);
build.mov(rArg1, rNonVol1);
build.call(rNonVol2);
// Epilogue
if (build.abi == ABIX64::Windows)
{
build.vmovaps(xmm6, xmmword[rsp + ((stackSize + localsSize) - 0x40)]);
build.vmovaps(xmm7, xmmword[rsp + ((stackSize + localsSize) - 0x30)]);
build.vmovaps(xmm8, xmmword[rsp + ((stackSize + localsSize) - 0x20)]);
build.vmovaps(xmm9, xmmword[rsp + ((stackSize + localsSize) - 0x10)]);
}
build.add(rsp, stackSize + localsSize);
build.pop(rbp);
build.pop(rNonVol2);
build.pop(rNonVol1);
build.ret();
unwind->finishFunction(build.getLabelOffset(functionBegin), ~0u);
build.finalize();
unwind->finishInfo();
size_t blockSize = 1024 * 1024;
size_t maxTotalSize = 1024 * 1024;
CodeAllocator allocator(blockSize, maxTotalSize);
allocator.context = unwind.get();
allocator.createBlockUnwindInfo = createBlockUnwindInfo;
allocator.destroyBlockUnwindInfo = destroyBlockUnwindInfo;
uint8_t* nativeData;
size_t sizeNativeData;
uint8_t* nativeEntry;
REQUIRE(allocator.allocate(build.data.data(), build.data.size(), build.code.data(), build.code.size(), nativeData, sizeNativeData, nativeEntry));
REQUIRE(nativeEntry);
using FunctionType = int64_t(int64_t, void (*)(int64_t));
FunctionType* f = (FunctionType*)nativeEntry;
f(10, nonthrowing);
obscureThrowCase(f);
}
TEST_CASE("GeneratedCodeExecutionMultipleFunctionsWithThrowX64") TEST_CASE("GeneratedCodeExecutionMultipleFunctionsWithThrowX64")
{ {
using namespace X64; using namespace X64;
@ -425,7 +547,7 @@ TEST_CASE("GeneratedCodeExecutionMultipleFunctionsWithThrowX64")
uint32_t prologueSize = build.setLabel().location - start1.location; uint32_t prologueSize = build.setLabel().location - start1.location;
unwind->prologueX64(prologueSize, stackSize + localsSize, /* setupFrame= */ true, {rNonVol1, rNonVol2}); unwind->prologueX64(prologueSize, stackSize + localsSize, /* setupFrame= */ true, {rNonVol1, rNonVol2}, {});
// Body // Body
build.mov(rNonVol1, rArg1); build.mov(rNonVol1, rArg1);
@ -464,7 +586,7 @@ TEST_CASE("GeneratedCodeExecutionMultipleFunctionsWithThrowX64")
uint32_t prologueSize = build.setLabel().location - start2.location; uint32_t prologueSize = build.setLabel().location - start2.location;
unwind->prologueX64(prologueSize, stackSize + localsSize, /* setupFrame= */ false, {rNonVol1, rNonVol2, rNonVol3, rNonVol4}); unwind->prologueX64(prologueSize, stackSize + localsSize, /* setupFrame= */ false, {rNonVol1, rNonVol2, rNonVol3, rNonVol4}, {});
// Body // Body
build.mov(rNonVol3, rArg1); build.mov(rNonVol3, rArg1);
@ -561,7 +683,7 @@ TEST_CASE("GeneratedCodeExecutionWithThrowOutsideTheGateX64")
uint32_t prologueSize = build.setLabel().location; uint32_t prologueSize = build.setLabel().location;
unwind->prologueX64(prologueSize, stackSize + localsSize, /* setupFrame= */ true, {r10, r11, r12, r13, r14, r15}); unwind->prologueX64(prologueSize, stackSize + localsSize, /* setupFrame= */ true, {r10, r11, r12, r13, r14, r15}, {});
// Body // Body
build.mov(rax, rArg1); build.mov(rax, rArg1);

View File

@ -63,6 +63,35 @@ static std::string compileTypeTable(const char* source)
TEST_SUITE_BEGIN("Compiler"); TEST_SUITE_BEGIN("Compiler");
TEST_CASE("BytecodeIsStable")
{
// As noted in Bytecode.h, all enums used for bytecode storage and serialization are order-sensitive
// Adding entries in the middle will typically pass the tests but break compatibility
// This test codifies this by validating that in each enum, the last (or close-to-last) entry has a fixed encoding
// This test will need to get occasionally revised to "move" the checked enum entries forward as we ship newer versions
// When doing so, please add *new* checks for more recent bytecode versions and keep existing checks in place.
// Bytecode ops (serialized & in-memory)
CHECK(LOP_FASTCALL2K == 75); // bytecode v1
CHECK(LOP_JUMPXEQKS == 80); // bytecode v3
// Bytecode fastcall ids (serialized & in-memory)
// Note: these aren't strictly bound to specific bytecode versions, but must monotonically increase to keep backwards compat
CHECK(LBF_VECTOR == 54);
CHECK(LBF_TOSTRING == 63);
// Bytecode capture type (serialized & in-memory)
CHECK(LCT_UPVAL == 2); // bytecode v1
// Bytecode constants (serialized)
CHECK(LBC_CONSTANT_CLOSURE == 6); // bytecode v1
// Bytecode type encoding (serialized & in-memory)
// Note: these *can* change retroactively *if* type version is bumped, but probably shouldn't
LUAU_ASSERT(LBC_TYPE_VECTOR == 8); // type version 1
}
TEST_CASE("CompileToBytecode") TEST_CASE("CompileToBytecode")
{ {
Luau::BytecodeBuilder bcb; Luau::BytecodeBuilder bcb;
@ -5085,7 +5114,7 @@ RETURN R1 1
)"); )");
} }
TEST_CASE("InlineBasicProhibited") TEST_CASE("InlineProhibited")
{ {
// we can't inline variadic functions // we can't inline variadic functions
CHECK_EQ("\n" + compileFunction(R"( CHECK_EQ("\n" + compileFunction(R"(
@ -5125,6 +5154,66 @@ RETURN R1 1
)"); )");
} }
TEST_CASE("InlineProhibitedRecursion")
{
// we can't inline recursive invocations of functions in the functions
// this is actually profitable in certain cases, but it complicates the compiler as it means a local has multiple registers/values
// in this example, inlining is blocked because we're compiling fact() and we don't yet have the cost model / profitability data for fact()
CHECK_EQ("\n" + compileFunction(R"(
local function fact(n)
return if n <= 1 then 1 else fact(n-1)*n
end
return fact
)",
0, 2),
R"(
LOADN R2 1
JUMPIFNOTLE R0 R2 L0
LOADN R1 1
RETURN R1 1
L0: GETUPVAL R2 0
SUBK R3 R0 K0 [1]
CALL R2 1 1
MUL R1 R2 R0
RETURN R1 1
)");
// in this example, inlining of fact() succeeds, but the nested call to fact() fails since fact is already on the inline stack
CHECK_EQ("\n" + compileFunction(R"(
local function fact(n)
return if n <= 1 then 1 else fact(n-1)*n
end
local function factsafe(n)
assert(n >= 1)
return fact(n)
end
return factsafe
)",
1, 2),
R"(
LOADN R3 1
JUMPIFLE R3 R0 L0
LOADB R2 0 +1
L0: LOADB R2 1
L1: FASTCALL1 1 R2 L2
GETIMPORT R1 1 [assert]
CALL R1 1 0
L2: LOADN R2 1
JUMPIFNOTLE R0 R2 L3
LOADN R1 1
RETURN R1 1
L3: GETUPVAL R2 0
SUBK R3 R0 K2 [1]
CALL R2 1 1
MUL R1 R2 R0
RETURN R1 1
)");
}
TEST_CASE("InlineNestedLoops") TEST_CASE("InlineNestedLoops")
{ {
// functions with basic loops get inlined // functions with basic loops get inlined
@ -7252,10 +7341,31 @@ end
)"); )");
} }
TEST_CASE("TypeUnionIntersection")
{
CHECK_EQ("\n" + compileTypeTable(R"(
function myfunc(test: string | nil, foo: nil)
end
function myfunc2(test: string & nil, foo: nil)
end
function myfunc3(test: string | number, foo: nil)
end
function myfunc4(test: string & number, foo: nil)
end
)"),
R"(
0: function(string?, nil)
1: function(any, nil)
2: function(any, nil)
3: function(any, nil)
)");
}
TEST_CASE("BuiltinFoldMathK") TEST_CASE("BuiltinFoldMathK")
{ {
ScopedFastFlag sff("LuauCompileFoldMathK", true);
// we can fold math.pi at optimization level 2 // we can fold math.pi at optimization level 2
CHECK_EQ("\n" + compileFunction(R"( CHECK_EQ("\n" + compileFunction(R"(
function test() function test()

View File

@ -446,8 +446,6 @@ TEST_CASE_FIXTURE(FrontendFixture, "cycle_incremental_type_surface_longer")
TEST_CASE_FIXTURE(FrontendFixture, "cycle_incremental_type_surface_exports") TEST_CASE_FIXTURE(FrontendFixture, "cycle_incremental_type_surface_exports")
{ {
ScopedFastFlag luauFixCyclicModuleExports{"LuauFixCyclicModuleExports", true};
fileResolver.source["game/A"] = R"( fileResolver.source["game/A"] = R"(
local b = require(game.B) local b = require(game.B)
export type atype = { x: b.btype } export type atype = { x: b.btype }

View File

@ -900,7 +900,7 @@ TEST_CASE_FIXTURE(Fixture, "parse_interpolated_string_double_brace_begin")
} }
catch (const ParseErrors& e) catch (const ParseErrors& e)
{ {
CHECK_EQ("Double braces are not permitted within interpolated strings. Did you mean '\\{'?", e.getErrors().front().getMessage()); CHECK_EQ("Double braces are not permitted within interpolated strings; did you mean '\\{'?", e.getErrors().front().getMessage());
} }
} }
@ -915,7 +915,7 @@ TEST_CASE_FIXTURE(Fixture, "parse_interpolated_string_double_brace_mid")
} }
catch (const ParseErrors& e) catch (const ParseErrors& e)
{ {
CHECK_EQ("Double braces are not permitted within interpolated strings. Did you mean '\\{'?", e.getErrors().front().getMessage()); CHECK_EQ("Double braces are not permitted within interpolated strings; did you mean '\\{'?", e.getErrors().front().getMessage());
} }
} }
@ -933,7 +933,7 @@ TEST_CASE_FIXTURE(Fixture, "parse_interpolated_string_without_end_brace")
CHECK_EQ(e.getErrors().size(), 1); CHECK_EQ(e.getErrors().size(), 1);
auto error = e.getErrors().front(); auto error = e.getErrors().front();
CHECK_EQ("Malformed interpolated string, did you forget to add a '}'?", error.getMessage()); CHECK_EQ("Malformed interpolated string; did you forget to add a '}'?", error.getMessage());
return error.getLocation().begin.column; return error.getLocation().begin.column;
} }
}; };
@ -956,7 +956,7 @@ TEST_CASE_FIXTURE(Fixture, "parse_interpolated_string_without_end_brace_in_table
{ {
CHECK_EQ(e.getErrors().size(), 2); CHECK_EQ(e.getErrors().size(), 2);
CHECK_EQ("Malformed interpolated string, did you forget to add a '}'?", e.getErrors().front().getMessage()); CHECK_EQ("Malformed interpolated string; did you forget to add a '}'?", e.getErrors().front().getMessage());
CHECK_EQ("Expected '}' (to close '{' at line 2), got <eof>", e.getErrors().back().getMessage()); CHECK_EQ("Expected '}' (to close '{' at line 2), got <eof>", e.getErrors().back().getMessage());
} }
} }
@ -974,7 +974,7 @@ TEST_CASE_FIXTURE(Fixture, "parse_interpolated_string_mid_without_end_brace_in_t
{ {
CHECK_EQ(e.getErrors().size(), 2); CHECK_EQ(e.getErrors().size(), 2);
CHECK_EQ("Malformed interpolated string, did you forget to add a '}'?", e.getErrors().front().getMessage()); CHECK_EQ("Malformed interpolated string; did you forget to add a '}'?", e.getErrors().front().getMessage());
CHECK_EQ("Expected '}' (to close '{' at line 2), got <eof>", e.getErrors().back().getMessage()); CHECK_EQ("Expected '}' (to close '{' at line 2), got <eof>", e.getErrors().back().getMessage());
} }
} }
@ -1041,6 +1041,36 @@ TEST_CASE_FIXTURE(Fixture, "parse_interpolated_string_without_expression")
} }
} }
TEST_CASE_FIXTURE(Fixture, "parse_interpolated_string_malformed_escape")
{
try
{
parse(R"(
local a = `???\xQQ {1}`
)");
FAIL("Expected ParseErrors to be thrown");
}
catch (const ParseErrors& e)
{
CHECK_EQ("Interpolated string literal contains malformed escape sequence", e.getErrors().front().getMessage());
}
}
TEST_CASE_FIXTURE(Fixture, "parse_interpolated_string_weird_token")
{
try
{
parse(R"(
local a = `??? {42 !!}`
)");
FAIL("Expected ParseErrors to be thrown");
}
catch (const ParseErrors& e)
{
CHECK_EQ("Malformed interpolated string, got '!'", e.getErrors().front().getMessage());
}
}
TEST_CASE_FIXTURE(Fixture, "parse_nesting_based_end_detection") TEST_CASE_FIXTURE(Fixture, "parse_nesting_based_end_detection")
{ {
try try
@ -1569,9 +1599,9 @@ TEST_CASE_FIXTURE(Fixture, "string_literals_escapes_broken")
TEST_CASE_FIXTURE(Fixture, "string_literals_broken") TEST_CASE_FIXTURE(Fixture, "string_literals_broken")
{ {
matchParseError("return \"", "Malformed string"); matchParseError("return \"", "Malformed string; did you forget to finish it?");
matchParseError("return \"\\", "Malformed string"); matchParseError("return \"\\", "Malformed string; did you forget to finish it?");
matchParseError("return \"\r\r", "Malformed string"); matchParseError("return \"\r\r", "Malformed string; did you forget to finish it?");
} }
TEST_CASE_FIXTURE(Fixture, "number_literals") TEST_CASE_FIXTURE(Fixture, "number_literals")
@ -2530,12 +2560,12 @@ TEST_CASE_FIXTURE(Fixture, "incomplete_method_call_still_yields_an_AstExprIndexN
TEST_CASE_FIXTURE(Fixture, "recover_confusables") TEST_CASE_FIXTURE(Fixture, "recover_confusables")
{ {
// Binary // Binary
matchParseError("local a = 4 != 10", "Unexpected '!=', did you mean '~='?"); matchParseError("local a = 4 != 10", "Unexpected '!='; did you mean '~='?");
matchParseError("local a = true && false", "Unexpected '&&', did you mean 'and'?"); matchParseError("local a = true && false", "Unexpected '&&'; did you mean 'and'?");
matchParseError("local a = false || true", "Unexpected '||', did you mean 'or'?"); matchParseError("local a = false || true", "Unexpected '||'; did you mean 'or'?");
// Unary // Unary
matchParseError("local a = !false", "Unexpected '!', did you mean 'not'?"); matchParseError("local a = !false", "Unexpected '!'; did you mean 'not'?");
// Check that separate tokens are not considered as a single one // Check that separate tokens are not considered as a single one
matchParseError("local a = 4 ! = 10", "Expected identifier when parsing expression, got '!'"); matchParseError("local a = 4 ! = 10", "Expected identifier when parsing expression, got '!'");
@ -2880,4 +2910,64 @@ TEST_CASE_FIXTURE(Fixture, "missing_default_type_pack_argument_after_variadic_ty
CHECK_EQ("Expected type pack after '=', got type", result.errors[1].getMessage()); CHECK_EQ("Expected type pack after '=', got type", result.errors[1].getMessage());
} }
TEST_CASE_FIXTURE(Fixture, "table_type_keys_cant_contain_nul")
{
ParseResult result = tryParse(R"(
type Foo = { ["\0"]: number }
)");
REQUIRE_EQ(1, result.errors.size());
CHECK_EQ(Location{{1, 21}, {1, 22}}, result.errors[0].getLocation());
CHECK_EQ("String literal contains malformed escape sequence or \\0", result.errors[0].getMessage());
}
TEST_CASE_FIXTURE(Fixture, "invalid_escape_literals_get_reported_but_parsing_continues")
{
ParseResult result = tryParse(R"(
local foo = "\xQQ"
print(foo)
)");
REQUIRE_EQ(1, result.errors.size());
CHECK_EQ(Location{{1, 20}, {1, 26}}, result.errors[0].getLocation());
CHECK_EQ("String literal contains malformed escape sequence", result.errors[0].getMessage());
REQUIRE(result.root);
CHECK_EQ(result.root->body.size, 2);
}
TEST_CASE_FIXTURE(Fixture, "unfinished_string_literals_get_reported_but_parsing_continues")
{
ParseResult result = tryParse(R"(
local foo = "hi
print(foo)
)");
REQUIRE_EQ(1, result.errors.size());
CHECK_EQ(Location{{1, 20}, {1, 23}}, result.errors[0].getLocation());
CHECK_EQ("Malformed string; did you forget to finish it?", result.errors[0].getMessage());
REQUIRE(result.root);
CHECK_EQ(result.root->body.size, 2);
}
TEST_CASE_FIXTURE(Fixture, "unfinished_string_literal_types_get_reported_but_parsing_continues")
{
ParseResult result = tryParse(R"(
type Foo = "hi
print(foo)
)");
REQUIRE_EQ(1, result.errors.size());
CHECK_EQ(Location{{1, 19}, {1, 22}}, result.errors[0].getLocation());
CHECK_EQ("Malformed string; did you forget to finish it?", result.errors[0].getMessage());
REQUIRE(result.root);
CHECK_EQ(result.root->body.size, 2);
}
TEST_SUITE_END(); TEST_SUITE_END();

View File

@ -4,16 +4,18 @@
#include "Fixture.h" #include "Fixture.h"
#include "Luau/Subtyping.h" #include "Luau/Subtyping.h"
#include "Luau/TypePack.h"
using namespace Luau; using namespace Luau;
struct SubtypeFixture : Fixture struct SubtypeFixture : Fixture
{ {
TypeArena arena; TypeArena arena;
InternalErrorReporter ice; InternalErrorReporter iceReporter;
UnifierSharedState sharedState{&ice}; UnifierSharedState sharedState{&ice};
Normalizer normalizer{&arena, builtinTypes, NotNull{&sharedState}}; Normalizer normalizer{&arena, builtinTypes, NotNull{&sharedState}};
Subtyping subtyping{builtinTypes, NotNull{&normalizer}};
Subtyping subtyping{builtinTypes, NotNull{&arena}, NotNull{&normalizer}, NotNull{&iceReporter}};
TypePackId pack(std::initializer_list<TypeId> tys) TypePackId pack(std::initializer_list<TypeId> tys)
{ {
@ -45,7 +47,28 @@ struct SubtypeFixture : Fixture
return arena.addType(FunctionType{pack(argHead, std::move(argTail)), pack(retHead, std::move(retTail))}); return arena.addType(FunctionType{pack(argHead, std::move(argTail)), pack(retHead, std::move(retTail))});
} }
SubtypingGraph isSubtype(TypeId subTy, TypeId superTy) TypeId tbl(TableType::Props&& props)
{
return arena.addType(TableType{std::move(props), std::nullopt, {}, TableState::Sealed});
}
TypeId cyclicTable(std::function<void(TypeId, TableType*)>&& cb)
{
TypeId res = arena.addType(GenericType{});
TableType tt{};
cb(res, &tt);
emplaceType<TableType>(asMutable(res), std::move(tt));
return res;
}
TypeId genericT = arena.addType(GenericType{"T"});
TypeId genericU = arena.addType(GenericType{"U"});
TypePackId genericAs = arena.addTypePack(GenericTypePack{"A"});
TypePackId genericBs = arena.addTypePack(GenericTypePack{"B"});
TypePackId genericCs = arena.addTypePack(GenericTypePack{"C"});
SubtypingResult isSubtype(TypeId subTy, TypeId superTy)
{ {
return subtyping.isSubtype(subTy, superTy); return subtyping.isSubtype(subTy, superTy);
} }
@ -57,7 +80,16 @@ struct SubtypeFixture : Fixture
TypeId helloOrWorldType = arena.addType(UnionType{{helloType, worldType}}); TypeId helloOrWorldType = arena.addType(UnionType{{helloType, worldType}});
TypeId trueOrFalseType = arena.addType(UnionType{{builtinTypes->trueType, builtinTypes->falseType}}); TypeId trueOrFalseType = arena.addType(UnionType{{builtinTypes->trueType, builtinTypes->falseType}});
// "hello" | "hello"
TypeId helloOrHelloType = arena.addType(UnionType{{helloType, helloType}});
// () -> ()
const TypeId nothingToNothingType = fn({}, {});
// ("hello") -> "world"
TypeId helloAndWorldType = arena.addType(IntersectionType{{helloType, worldType}}); TypeId helloAndWorldType = arena.addType(IntersectionType{{helloType, worldType}});
// (boolean) -> true
TypeId booleanAndTrueType = arena.addType(IntersectionType{{builtinTypes->booleanType, builtinTypes->trueType}}); TypeId booleanAndTrueType = arena.addType(IntersectionType{{builtinTypes->booleanType, builtinTypes->trueType}});
// (number) -> string // (number) -> string
@ -72,6 +104,24 @@ struct SubtypeFixture : Fixture
{builtinTypes->stringType} {builtinTypes->stringType}
); );
// (number) -> ()
const TypeId numberToNothingType = fn(
{builtinTypes->numberType},
{}
);
// () -> number
const TypeId nothingToNumberType = fn(
{},
{builtinTypes->numberType}
);
// (number) -> number
const TypeId numberToNumberType = fn(
{builtinTypes->numberType},
{builtinTypes->numberType}
);
// (number) -> unknown // (number) -> unknown
const TypeId numberToUnknownType = fn( const TypeId numberToUnknownType = fn(
{builtinTypes->numberType}, {builtinTypes->numberType},
@ -120,6 +170,83 @@ struct SubtypeFixture : Fixture
{builtinTypes->stringType} {builtinTypes->stringType}
); );
// (...number) -> number
const TypeId numbersToNumberType = arena.addType(FunctionType{
arena.addTypePack(VariadicTypePack{builtinTypes->numberType}),
arena.addTypePack({builtinTypes->numberType})
});
// <T>(T) -> ()
const TypeId genericTToNothingType = arena.addType(FunctionType{
{genericT},
{},
arena.addTypePack({genericT}),
builtinTypes->emptyTypePack
});
// <T>(T) -> T
const TypeId genericTToTType = arena.addType(FunctionType{
{genericT},
{},
arena.addTypePack({genericT}),
arena.addTypePack({genericT})
});
// <U>(U) -> ()
const TypeId genericUToNothingType = arena.addType(FunctionType{
{genericU},
{},
arena.addTypePack({genericU}),
builtinTypes->emptyTypePack
});
// <T>() -> T
const TypeId genericNothingToTType = arena.addType(FunctionType{
{genericT},
{},
builtinTypes->emptyTypePack,
arena.addTypePack({genericT})
});
// <A...>(A...) -> A...
const TypeId genericAsToAsType = arena.addType(FunctionType{
{},
{genericAs},
genericAs,
genericAs
});
// <A...>(A...) -> number
const TypeId genericAsToNumberType = arena.addType(FunctionType{
{},
{genericAs},
genericAs,
arena.addTypePack({builtinTypes->numberType})
});
// <B...>(B...) -> B...
const TypeId genericBsToBsType = arena.addType(FunctionType{
{},
{genericBs},
genericBs,
genericBs
});
// <B..., C...>(B...) -> C...
const TypeId genericBsToCsType = arena.addType(FunctionType{
{},
{genericBs, genericCs},
genericBs,
genericCs
});
// <A...>() -> A...
const TypeId genericNothingToAsType = arena.addType(FunctionType{
{},
{genericAs},
builtinTypes->emptyTypePack,
genericAs
});
}; };
#define CHECK_IS_SUBTYPE(left, right) \ #define CHECK_IS_SUBTYPE(left, right) \
@ -127,7 +254,7 @@ struct SubtypeFixture : Fixture
{ \ { \
const auto& leftTy = (left); \ const auto& leftTy = (left); \
const auto& rightTy = (right); \ const auto& rightTy = (right); \
SubtypingGraph result = isSubtype(leftTy, rightTy); \ SubtypingResult result = isSubtype(leftTy, rightTy); \
CHECK_MESSAGE(result.isSubtype, "Expected " << leftTy << " <: " << rightTy); \ CHECK_MESSAGE(result.isSubtype, "Expected " << leftTy << " <: " << rightTy); \
} while (0) } while (0)
@ -136,7 +263,7 @@ struct SubtypeFixture : Fixture
{ \ { \
const auto& leftTy = (left); \ const auto& leftTy = (left); \
const auto& rightTy = (right); \ const auto& rightTy = (right); \
SubtypingGraph result = isSubtype(leftTy, rightTy); \ SubtypingResult result = isSubtype(leftTy, rightTy); \
CHECK_MESSAGE(!result.isSubtype, "Expected " << leftTy << " </: " << rightTy); \ CHECK_MESSAGE(!result.isSubtype, "Expected " << leftTy << " </: " << rightTy); \
} while (0) } while (0)
@ -145,7 +272,7 @@ struct SubtypeFixture : Fixture
{ \ { \
const auto& leftTy = (left); \ const auto& leftTy = (left); \
const auto& rightTy = (right); \ const auto& rightTy = (right); \
SubtypingGraph result = isSubtype(leftTy, rightTy); \ SubtypingResult result = isSubtype(leftTy, rightTy); \
CHECK_MESSAGE(result.isErrorSuppressing, "Expected " << leftTy << " to error-suppress " << rightTy); \ CHECK_MESSAGE(result.isErrorSuppressing, "Expected " << leftTy << " to error-suppress " << rightTy); \
} while (0) } while (0)
@ -154,7 +281,7 @@ struct SubtypeFixture : Fixture
{ \ { \
const auto& leftTy = (left); \ const auto& leftTy = (left); \
const auto& rightTy = (right); \ const auto& rightTy = (right); \
SubtypingGraph result = isSubtype(leftTy, rightTy); \ SubtypingResult result = isSubtype(leftTy, rightTy); \
CHECK_MESSAGE(!result.isErrorSuppressing, "Expected " << leftTy << " to error-suppress " << rightTy); \ CHECK_MESSAGE(!result.isErrorSuppressing, "Expected " << leftTy << " to error-suppress " << rightTy); \
} while (0) } while (0)
@ -169,7 +296,7 @@ TEST_CASE_FIXTURE(SubtypeFixture, "number <: any")
TEST_CASE_FIXTURE(SubtypeFixture, "any <!: unknown") TEST_CASE_FIXTURE(SubtypeFixture, "any <!: unknown")
{ {
SubtypingGraph result = isSubtype(builtinTypes->anyType, builtinTypes->unknownType); SubtypingResult result = isSubtype(builtinTypes->anyType, builtinTypes->unknownType);
CHECK(!result.isSubtype); CHECK(!result.isSubtype);
CHECK(result.isErrorSuppressing); CHECK(result.isErrorSuppressing);
} }
@ -244,6 +371,11 @@ TEST_CASE_FIXTURE(SubtypeFixture, "\"hello\" | \"world\" <: number")
CHECK_IS_NOT_SUBTYPE(helloOrWorldType, builtinTypes->numberType); CHECK_IS_NOT_SUBTYPE(helloOrWorldType, builtinTypes->numberType);
} }
TEST_CASE_FIXTURE(SubtypeFixture, "string <!: ('hello' | 'hello')")
{
CHECK_IS_NOT_SUBTYPE(builtinTypes->stringType, helloOrHelloType);
}
TEST_CASE_FIXTURE(SubtypeFixture, "true <: boolean & true") TEST_CASE_FIXTURE(SubtypeFixture, "true <: boolean & true")
{ {
CHECK_IS_SUBTYPE(builtinTypes->trueType, booleanAndTrueType); CHECK_IS_SUBTYPE(builtinTypes->trueType, booleanAndTrueType);
@ -349,4 +481,206 @@ TEST_CASE_FIXTURE(SubtypeFixture, "(number, string) -> string <!: (number, ...st
CHECK_IS_NOT_SUBTYPE(numberAndStringToStringType, numberAndStringsToStringType); CHECK_IS_NOT_SUBTYPE(numberAndStringToStringType, numberAndStringsToStringType);
} }
TEST_CASE_FIXTURE(SubtypeFixture, "<T>() -> T <: () -> number")
{
CHECK_IS_SUBTYPE(genericNothingToTType, nothingToNumberType);
}
TEST_CASE_FIXTURE(SubtypeFixture, "<T>(T) -> () <: <U>(U) -> ()")
{
CHECK_IS_SUBTYPE(genericTToNothingType, genericUToNothingType);
}
TEST_CASE_FIXTURE(SubtypeFixture, "() -> number <!: <T>() -> T")
{
CHECK_IS_NOT_SUBTYPE(nothingToNumberType, genericNothingToTType);
}
TEST_CASE_FIXTURE(SubtypeFixture, "<T>(T) -> () <: (number) -> ()")
{
CHECK_IS_SUBTYPE(genericTToNothingType, numberToNothingType);
}
TEST_CASE_FIXTURE(SubtypeFixture, "<T>(T) -> T <: (number) -> number")
{
CHECK_IS_SUBTYPE(genericTToTType, numberToNumberType);
}
TEST_CASE_FIXTURE(SubtypeFixture, "<T>(T) -> T <!: (number) -> string")
{
CHECK_IS_NOT_SUBTYPE(genericTToTType, numberToStringType);
}
TEST_CASE_FIXTURE(SubtypeFixture, "<T>(T) -> () <: <U>(U) -> ()")
{
CHECK_IS_SUBTYPE(genericTToNothingType, genericUToNothingType);
}
TEST_CASE_FIXTURE(SubtypeFixture, "(number) -> () <!: <T>(T) -> ()")
{
CHECK_IS_NOT_SUBTYPE(numberToNothingType, genericTToNothingType);
}
TEST_CASE_FIXTURE(SubtypeFixture, "<A...>(A...) -> A... <: (number) -> number")
{
CHECK_IS_SUBTYPE(genericAsToAsType, numberToNumberType);
}
TEST_CASE_FIXTURE(SubtypeFixture, "(number) -> number <!: <A...>(A...) -> A...")
{
CHECK_IS_NOT_SUBTYPE(numberToNumberType, genericAsToAsType);
}
TEST_CASE_FIXTURE(SubtypeFixture, "<A...>(A...) -> A... <: <B...>(B...) -> B...")
{
CHECK_IS_SUBTYPE(genericAsToAsType, genericBsToBsType);
}
TEST_CASE_FIXTURE(SubtypeFixture, "<B..., C...>(B...) -> C... <: <A...>(A...) -> A...")
{
CHECK_IS_SUBTYPE(genericBsToCsType, genericAsToAsType);
}
TEST_CASE_FIXTURE(SubtypeFixture, "<A...>(A...) -> A... <!: <B..., C...>(B...) -> C...")
{
CHECK_IS_NOT_SUBTYPE(genericAsToAsType, genericBsToCsType);
}
TEST_CASE_FIXTURE(SubtypeFixture, "<A...>(A...) -> number <: (number) -> number")
{
CHECK_IS_SUBTYPE(genericAsToNumberType, numberToNumberType);
}
TEST_CASE_FIXTURE(SubtypeFixture, "(number) -> number <!: <A...>(A...) -> number")
{
CHECK_IS_NOT_SUBTYPE(numberToNumberType, genericAsToNumberType);
}
TEST_CASE_FIXTURE(SubtypeFixture, "<A...>(A...) -> number <: (...number) -> number")
{
CHECK_IS_SUBTYPE(genericAsToNumberType, numbersToNumberType);
}
TEST_CASE_FIXTURE(SubtypeFixture, "(...number) -> number <!: <A...>(A...) -> number")
{
CHECK_IS_NOT_SUBTYPE(numbersToNumberType, genericAsToNumberType);
}
TEST_CASE_FIXTURE(SubtypeFixture, "<A...>() -> A... <: () -> ()")
{
CHECK_IS_SUBTYPE(genericNothingToAsType, nothingToNothingType);
}
TEST_CASE_FIXTURE(SubtypeFixture, "() -> () <!: <A...>() -> A...")
{
CHECK_IS_NOT_SUBTYPE(nothingToNothingType, genericNothingToAsType);
}
TEST_CASE_FIXTURE(SubtypeFixture, "<A...>(A...) -> A... <: () -> ()")
{
CHECK_IS_SUBTYPE(genericAsToAsType, nothingToNothingType);
}
TEST_CASE_FIXTURE(SubtypeFixture, "() -> () <!: <A...>(A...) -> A...")
{
CHECK_IS_NOT_SUBTYPE(nothingToNothingType, genericAsToAsType);
}
TEST_CASE_FIXTURE(SubtypeFixture, "{} <: {}")
{
CHECK_IS_SUBTYPE(tbl({}), tbl({}));
}
TEST_CASE_FIXTURE(SubtypeFixture, "{x: number} <: {}")
{
CHECK_IS_SUBTYPE(tbl({{"x", builtinTypes->numberType}}), tbl({}));
}
TEST_CASE_FIXTURE(SubtypeFixture, "{x: number} <!: {x: string}")
{
CHECK_IS_NOT_SUBTYPE(tbl({{"x", builtinTypes->numberType}}), tbl({{"x", builtinTypes->stringType}}));
}
TEST_CASE_FIXTURE(SubtypeFixture, "{x: number} <!: {x: number?}")
{
CHECK_IS_NOT_SUBTYPE(tbl({{"x", builtinTypes->numberType}}), tbl({{"x", builtinTypes->optionalNumberType}}));
}
TEST_CASE_FIXTURE(SubtypeFixture, "{x: number?} <!: {x: number}")
{
CHECK_IS_NOT_SUBTYPE(tbl({{"x", builtinTypes->optionalNumberType}}), tbl({{"x", builtinTypes->numberType}}));
}
TEST_CASE_FIXTURE(SubtypeFixture, "{x: <T>(T) -> ()} <: {x: <U>(U) -> ()}")
{
CHECK_IS_SUBTYPE(
tbl({{"x", genericTToNothingType}}),
tbl({{"x", genericUToNothingType}})
);
}
TEST_CASE_FIXTURE(SubtypeFixture, "t1 where t1 = {trim: (t1) -> string} <: t2 where t2 = {trim: (t2) -> string}")
{
TypeId t1 = cyclicTable([&](TypeId ty, TableType* tt)
{
tt->props["trim"] = fn({ty}, {builtinTypes->stringType});
});
TypeId t2 = cyclicTable([&](TypeId ty, TableType* tt)
{
tt->props["trim"] = fn({ty}, {builtinTypes->stringType});
});
CHECK_IS_SUBTYPE(t1, t2);
}
TEST_CASE_FIXTURE(SubtypeFixture, "t1 where t1 = {trim: (t1) -> string} <!: t2 where t2 = {trim: (t2) -> t2}")
{
TypeId t1 = cyclicTable([&](TypeId ty, TableType* tt)
{
tt->props["trim"] = fn({ty}, {builtinTypes->stringType});
});
TypeId t2 = cyclicTable([&](TypeId ty, TableType* tt)
{
tt->props["trim"] = fn({ty}, {ty});
});
CHECK_IS_NOT_SUBTYPE(t1, t2);
}
TEST_CASE_FIXTURE(SubtypeFixture, "t1 where t1 = {trim: (t1) -> t1} <!: t2 where t2 = {trim: (t2) -> string}")
{
TypeId t1 = cyclicTable([&](TypeId ty, TableType* tt)
{
tt->props["trim"] = fn({ty}, {ty});
});
TypeId t2 = cyclicTable([&](TypeId ty, TableType* tt)
{
tt->props["trim"] = fn({ty}, {builtinTypes->stringType});
});
CHECK_IS_NOT_SUBTYPE(t1, t2);
}
/*
* <A>(A) -> A <: <X>(X) -> X
* A can be bound to X.
*
* <A>(A) -> A </: <X>(X) -> number
* A can be bound to X, but A </: number
*
* (number) -> number </: <A>(A) -> A
* Only generics on the left side can be bound.
* number </: A
*
* <A, B>(A, B) -> boolean <: <X>(X, X) -> boolean
* It is ok to bind both A and B to X.
*
* <A>(A, A) -> boolean </: <X, Y>(X, Y) -> boolean
* A cannot be bound to both X and Y.
*/
TEST_SUITE_END(); TEST_SUITE_END();

View File

@ -1017,4 +1017,54 @@ local y = x["Bar"]
LUAU_REQUIRE_NO_ERRORS(result); LUAU_REQUIRE_NO_ERRORS(result);
} }
TEST_CASE_FIXTURE(Fixture, "cli_80596_simplify_degenerate_intersections")
{
ScopedFastFlag dcr{"DebugLuauDeferredConstraintResolution", true};
CheckResult result = check(R"(
type A = {
x: number?,
}
type B = {
x: number?,
}
type C = A & B
local obj: C = {
x = 3,
}
local x: number = obj.x or 3
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "cli_80596_simplify_more_realistic_intersections")
{
ScopedFastFlag dcr{"DebugLuauDeferredConstraintResolution", true};
CheckResult result = check(R"(
type A = {
x: number?,
y: string?,
}
type B = {
x: number?,
z: string?,
}
type C = A & B
local obj: C = {
x = 3,
}
local x: number = obj.x or 3
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_SUITE_END(); TEST_SUITE_END();