Sync to upstream/release/579 (#943)

A pretty small changelist this week:

* When type inference fails to find any matching overload for a
function, we were declining to commit any changes to the type graph at
all. This was resulting in confusing type errors in certain cases. Now,
when a matching overload cannot be found, we always commit to the first
overload we tried.

JIT

* Fix missing variadic register invalidation in FALLBACK_GETVARARGS
* Add a missing null pointer check for the result of luaT_gettm

---------

Co-authored-by: Arseny Kapoulkine <arseny.kapoulkine@gmail.com>
Co-authored-by: Vyacheslav Egorov <vegorov@roblox.com>
This commit is contained in:
Andy Friesen 2023-06-02 12:52:15 -07:00 committed by GitHub
parent 271c509046
commit 63679f7288
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 597 additions and 461 deletions

View File

@ -30,7 +30,14 @@ struct ModuleResolver;
using Name = std::string;
using ScopePtr = std::shared_ptr<Scope>;
using OverloadErrorEntry = std::tuple<std::vector<TypeError>, std::vector<TypeId>, const FunctionType*>;
struct OverloadErrorEntry
{
TxnLog log;
ErrorVec errors;
std::vector<TypeId> arguments;
const FunctionType* fnTy;
};
bool doesCallError(const AstExprCall* call);
bool hasBreak(AstStat* node);
@ -166,7 +173,7 @@ struct TypeChecker
const std::vector<OverloadErrorEntry>& errors);
void reportOverloadResolutionError(const ScopePtr& scope, const AstExprCall& expr, TypePackId retPack, TypePackId argPack,
const std::vector<Location>& argLocations, const std::vector<TypeId>& overloads, const std::vector<TypeId>& overloadsThatMatchArgCount,
const std::vector<OverloadErrorEntry>& errors);
std::vector<OverloadErrorEntry>& errors);
WithPredicate<TypePackId> checkExprList(const ScopePtr& scope, const Location& location, const AstArray<AstExpr*>& exprs,
bool substituteFreeForNil = false, const std::vector<bool>& lhsAnnotations = {},

View File

@ -1300,6 +1300,7 @@ bool ConstraintSolver::tryDispatch(const FunctionCallConstraint& c, NotNull<cons
Instantiation inst(TxnLog::empty(), arena, TypeLevel{}, constraint->scope);
std::vector<TypeId> arityMatchingOverloads;
std::optional<TxnLog> bestOverloadLog;
for (TypeId overload : overloads)
{
@ -1330,29 +1331,24 @@ bool ConstraintSolver::tryDispatch(const FunctionCallConstraint& c, NotNull<cons
if (const auto& e = hasUnificationTooComplex(u.errors))
reportError(*e);
if (const auto& e = hasCountMismatch(u.errors);
(!e || get<CountMismatch>(*e)->context != CountMismatch::Context::Arg) && get<FunctionType>(*instantiated))
{
const auto& e = hasCountMismatch(u.errors);
bool areArgumentsCompatible = (!e || get<CountMismatch>(*e)->context != CountMismatch::Context::Arg) && get<FunctionType>(*instantiated);
if (areArgumentsCompatible)
arityMatchingOverloads.push_back(*instantiated);
}
if (u.errors.empty())
{
if (c.callSite)
(*c.astOverloadResolvedTypes)[c.callSite] = *instantiated;
// We found a matching overload.
const auto [changedTypes, changedPacks] = u.log.getChanges();
u.log.commit();
unblock(changedTypes);
unblock(changedPacks);
unblock(c.result);
InstantiationQueuer queuer{constraint->scope, constraint->location, this};
queuer.traverse(fn);
queuer.traverse(inferredTy);
return true;
// This overload has no errors, so override the bestOverloadLog and use this one.
bestOverloadLog = std::move(u.log);
break;
}
else if (areArgumentsCompatible && !bestOverloadLog)
{
// This overload is erroneous. Replace its inferences with `any` iff there isn't already a TxnLog.
bestOverloadLog = std::move(u.log);
}
}
@ -1365,15 +1361,20 @@ bool ConstraintSolver::tryDispatch(const FunctionCallConstraint& c, NotNull<cons
(*c.astOverloadResolvedTypes)[c.callSite] = arityMatchingOverloads.at(0);
}
// We found no matching overloads.
Unifier u{normalizer, constraint->scope, Location{}, Covariant};
u.enableScopeTests();
// We didn't find any overload that were a viable candidate, so replace the inferences with `any`.
if (!bestOverloadLog)
{
Unifier u{normalizer, constraint->scope, Location{}, Covariant};
u.enableScopeTests();
u.tryUnify(inferredTy, builtinTypes->anyType);
u.tryUnify(fn, builtinTypes->anyType);
u.tryUnify(inferredTy, builtinTypes->anyType);
u.tryUnify(fn, builtinTypes->anyType);
const auto [changedTypes, changedPacks] = u.log.getChanges();
u.log.commit();
bestOverloadLog = std::move(u.log);
}
const auto [changedTypes, changedPacks] = bestOverloadLog->getChanges();
bestOverloadLog->commit();
unblock(changedTypes);
unblock(changedPacks);

View File

@ -111,94 +111,77 @@ void TxnLog::concatAsIntersections(TxnLog rhs, NotNull<TypeArena> arena)
void TxnLog::concatAsUnion(TxnLog rhs, NotNull<TypeArena> arena)
{
if (FFlag::DebugLuauDeferredConstraintResolution)
/*
* Check for cycles.
*
* We must not combine a log entry that binds 'a to 'b with a log that
* binds 'b to 'a.
*
* Of the two, identify the one with the 'bigger' scope and eliminate the
* entry that rebinds it.
*/
for (const auto& [rightTy, rightRep] : rhs.typeVarChanges)
{
/*
* Check for cycles.
*
* We must not combine a log entry that binds 'a to 'b with a log that
* binds 'b to 'a.
*
* Of the two, identify the one with the 'bigger' scope and eliminate the
* entry that rebinds it.
*/
for (const auto& [rightTy, rightRep] : rhs.typeVarChanges)
if (rightRep->dead)
continue;
// We explicitly use get_if here because we do not wish to do anything
// if the uncommitted type is already bound to something else.
const FreeType* rf = get_if<FreeType>(&rightTy->ty);
if (!rf)
continue;
const BoundType* rb = Luau::get<BoundType>(&rightRep->pending);
if (!rb)
continue;
const TypeId leftTy = rb->boundTo;
const FreeType* lf = get_if<FreeType>(&leftTy->ty);
if (!lf)
continue;
auto leftRep = typeVarChanges.find(leftTy);
if (!leftRep)
continue;
if ((*leftRep)->dead)
continue;
const BoundType* lb = Luau::get<BoundType>(&(*leftRep)->pending);
if (!lb)
continue;
if (lb->boundTo == rightTy)
{
if (rightRep->dead)
continue;
// leftTy has been bound to rightTy, but rightTy has also been bound
// to leftTy. We find the one that belongs to the more deeply nested
// scope and remove it from the log.
const bool discardLeft = useScopes ? subsumes(lf->scope, rf->scope) : lf->level.subsumes(rf->level);
// We explicitly use get_if here because we do not wish to do anything
// if the uncommitted type is already bound to something else.
const FreeType* rf = get_if<FreeType>(&rightTy->ty);
if (!rf)
continue;
const BoundType* rb = Luau::get<BoundType>(&rightRep->pending);
if (!rb)
continue;
const TypeId leftTy = rb->boundTo;
const FreeType* lf = get_if<FreeType>(&leftTy->ty);
if (!lf)
continue;
auto leftRep = typeVarChanges.find(leftTy);
if (!leftRep)
continue;
if ((*leftRep)->dead)
continue;
const BoundType* lb = Luau::get<BoundType>(&(*leftRep)->pending);
if (!lb)
continue;
if (lb->boundTo == rightTy)
{
// leftTy has been bound to rightTy, but rightTy has also been bound
// to leftTy. We find the one that belongs to the more deeply nested
// scope and remove it from the log.
const bool discardLeft = useScopes ? subsumes(lf->scope, rf->scope) : lf->level.subsumes(rf->level);
if (discardLeft)
(*leftRep)->dead = true;
else
rightRep->dead = true;
}
}
for (auto& [ty, rightRep] : rhs.typeVarChanges)
{
if (rightRep->dead)
continue;
if (auto leftRep = typeVarChanges.find(ty); leftRep && !(*leftRep)->dead)
{
TypeId leftTy = arena->addType((*leftRep)->pending);
TypeId rightTy = arena->addType(rightRep->pending);
if (follow(leftTy) == follow(rightTy))
typeVarChanges[ty] = std::move(rightRep);
else
typeVarChanges[ty]->pending.ty = UnionType{{leftTy, rightTy}};
}
if (discardLeft)
(*leftRep)->dead = true;
else
typeVarChanges[ty] = std::move(rightRep);
rightRep->dead = true;
}
}
else
for (auto& [ty, rightRep] : rhs.typeVarChanges)
{
for (auto& [ty, rightRep] : rhs.typeVarChanges)
if (rightRep->dead)
continue;
if (auto leftRep = typeVarChanges.find(ty); leftRep && !(*leftRep)->dead)
{
if (auto leftRep = typeVarChanges.find(ty))
{
TypeId leftTy = arena->addType((*leftRep)->pending);
TypeId rightTy = arena->addType(rightRep->pending);
typeVarChanges[ty]->pending.ty = UnionType{{leftTy, rightTy}};
}
else
TypeId leftTy = arena->addType((*leftRep)->pending);
TypeId rightTy = arena->addType(rightRep->pending);
if (follow(leftTy) == follow(rightTy))
typeVarChanges[ty] = std::move(rightRep);
else
typeVarChanges[ty]->pending.ty = UnionType{{leftTy, rightTy}};
}
else
typeVarChanges[ty] = std::move(rightRep);
}
for (auto& [tp, rep] : rhs.typePackChanges)

View File

@ -347,14 +347,18 @@ TypeFamilyReductionResult<TypeId> addFamilyFn(std::vector<TypeId> typeParams, st
const NormalizedType* normLhsTy = normalizer->normalize(lhsTy);
const NormalizedType* normRhsTy = normalizer->normalize(rhsTy);
if (normLhsTy && normRhsTy && normLhsTy->isNumber() && normRhsTy->isNumber())
if (!normLhsTy || !normRhsTy)
{
return {builtins->numberType, false, {}, {}};
return {std::nullopt, false, {}, {}};
}
else if (log->is<AnyType>(lhsTy) || log->is<AnyType>(rhsTy))
else if (log->is<AnyType>(normLhsTy->tops) || log->is<AnyType>(normRhsTy->tops))
{
return {builtins->anyType, false, {}, {}};
}
else if (normLhsTy->isNumber() && normRhsTy->isNumber())
{
return {builtins->numberType, false, {}, {}};
}
else if (log->is<ErrorType>(lhsTy) || log->is<ErrorType>(rhsTy))
{
return {builtins->errorRecoveryType(), false, {}, {}};

View File

@ -40,6 +40,7 @@ LUAU_FASTFLAG(LuauOccursIsntAlwaysFailure)
LUAU_FASTFLAGVARIABLE(LuauTypecheckTypeguards, false)
LUAU_FASTFLAGVARIABLE(LuauTinyControlFlowAnalysis, false)
LUAU_FASTFLAGVARIABLE(LuauTypecheckClassTypeIndexers, false)
LUAU_FASTFLAGVARIABLE(LuauAlwaysCommitInferencesOfFunctionCalls, false)
namespace Luau
{
@ -4387,7 +4388,12 @@ std::unique_ptr<WithPredicate<TypePackId>> TypeChecker::checkCallOverload(const
else
overloadsThatDont.push_back(fn);
errors.emplace_back(std::move(state.errors), args->head, ftv);
errors.push_back(OverloadErrorEntry{
std::move(state.log),
std::move(state.errors),
args->head,
ftv,
});
}
else
{
@ -4407,7 +4413,7 @@ bool TypeChecker::handleSelfCallMismatch(const ScopePtr& scope, const AstExprCal
{
// No overloads succeeded: Scan for one that would have worked had the user
// used a.b() rather than a:b() or vice versa.
for (const auto& [_, argVec, ftv] : errors)
for (const auto& e : errors)
{
// Did you write foo:bar() when you should have written foo.bar()?
if (expr.self)
@ -4418,7 +4424,7 @@ bool TypeChecker::handleSelfCallMismatch(const ScopePtr& scope, const AstExprCal
TypePackId editedArgPack = addTypePack(TypePack{editedParamList});
Unifier editedState = mkUnifier(scope, expr.location);
checkArgumentList(scope, *expr.func, editedState, editedArgPack, ftv->argTypes, editedArgLocations);
checkArgumentList(scope, *expr.func, editedState, editedArgPack, e.fnTy->argTypes, editedArgLocations);
if (editedState.errors.empty())
{
@ -4433,7 +4439,7 @@ bool TypeChecker::handleSelfCallMismatch(const ScopePtr& scope, const AstExprCal
return true;
}
}
else if (ftv->hasSelf)
else if (e.fnTy->hasSelf)
{
// Did you write foo.bar() when you should have written foo:bar()?
if (AstExprIndexName* indexName = expr.func->as<AstExprIndexName>())
@ -4449,7 +4455,7 @@ bool TypeChecker::handleSelfCallMismatch(const ScopePtr& scope, const AstExprCal
Unifier editedState = mkUnifier(scope, expr.location);
checkArgumentList(scope, *expr.func, editedState, editedArgPack, ftv->argTypes, editedArgLocations);
checkArgumentList(scope, *expr.func, editedState, editedArgPack, e.fnTy->argTypes, editedArgLocations);
if (editedState.errors.empty())
{
@ -4472,11 +4478,14 @@ bool TypeChecker::handleSelfCallMismatch(const ScopePtr& scope, const AstExprCal
void TypeChecker::reportOverloadResolutionError(const ScopePtr& scope, const AstExprCall& expr, TypePackId retPack, TypePackId argPack,
const std::vector<Location>& argLocations, const std::vector<TypeId>& overloads, const std::vector<TypeId>& overloadsThatMatchArgCount,
const std::vector<OverloadErrorEntry>& errors)
std::vector<OverloadErrorEntry>& errors)
{
if (overloads.size() == 1)
{
reportErrors(std::get<0>(errors.front()));
if (FFlag::LuauAlwaysCommitInferencesOfFunctionCalls)
errors.front().log.commit();
reportErrors(errors.front().errors);
return;
}
@ -4498,11 +4507,15 @@ void TypeChecker::reportOverloadResolutionError(const ScopePtr& scope, const Ast
const FunctionType* ftv = get<FunctionType>(overload);
auto error = std::find_if(errors.begin(), errors.end(), [ftv](const OverloadErrorEntry& e) {
return ftv == std::get<2>(e);
return ftv == e.fnTy;
});
LUAU_ASSERT(error != errors.end());
reportErrors(std::get<0>(*error));
if (FFlag::LuauAlwaysCommitInferencesOfFunctionCalls)
error->log.commit();
reportErrors(error->errors);
// If only one overload matched, we don't need this error because we provided the previous errors.
if (overloadsThatMatchArgCount.size() == 1)

View File

@ -21,13 +21,13 @@ LUAU_FASTFLAG(LuauErrorRecoveryType)
LUAU_FASTFLAGVARIABLE(LuauInstantiateInSubtyping, false)
LUAU_FASTFLAGVARIABLE(LuauUninhabitedSubAnything2, false)
LUAU_FASTFLAGVARIABLE(LuauVariadicAnyCanBeGeneric, false)
LUAU_FASTFLAGVARIABLE(LuauUnifyTwoOptions, false)
LUAU_FASTFLAGVARIABLE(LuauMaintainScopesInUnifier, false)
LUAU_FASTFLAGVARIABLE(LuauTransitiveSubtyping, false)
LUAU_FASTFLAGVARIABLE(LuauOccursIsntAlwaysFailure, false)
LUAU_FASTFLAG(LuauClassTypeVarsInSubstitution)
LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution)
LUAU_FASTFLAG(LuauNormalizeBlockedTypes)
LUAU_FASTFLAG(LuauAlwaysCommitInferencesOfFunctionCalls)
namespace Luau
{
@ -761,93 +761,8 @@ void Unifier::tryUnify_(TypeId subTy, TypeId superTy, bool isFunctionCall, bool
log.popSeen(superTy, subTy);
}
/*
* If the passed type is an option, strip nil out.
*
* There is an important subtlety to be observed here:
*
* We want to do a peephole fix to unify the subtype relation A? <: B? where we
* instead peel off the options and relate A <: B instead, but only works if we
* are certain that neither A nor B are themselves optional.
*
* For instance, if we want to test that (boolean?)? <: boolean?, we must peel
* off both layers of optionality from the subTy.
*
* We must also handle unions that have more than two choices.
*
* eg (string | nil)? <: boolean?
*/
static std::optional<TypeId> unwrapOption(NotNull<BuiltinTypes> builtinTypes, NotNull<TypeArena> arena, const TxnLog& log, TypeId ty, DenseHashSet<TypeId>& seen)
{
if (seen.find(ty))
return std::nullopt;
seen.insert(ty);
const UnionType* ut = get<UnionType>(follow(ty));
if (!ut)
return std::nullopt;
if (2 == ut->options.size())
{
if (isNil(follow(ut->options[0])))
{
std::optional<TypeId> doubleUnwrapped = unwrapOption(builtinTypes, arena, log, ut->options[1], seen);
return doubleUnwrapped.value_or(ut->options[1]);
}
if (isNil(follow(ut->options[1])))
{
std::optional<TypeId> doubleUnwrapped = unwrapOption(builtinTypes, arena, log, ut->options[0], seen);
return doubleUnwrapped.value_or(ut->options[0]);
}
}
std::set<TypeId> newOptions;
bool found = false;
for (TypeId t : ut)
{
t = log.follow(t);
if (isNil(t))
{
found = true;
continue;
}
else
newOptions.insert(t);
}
if (!found)
return std::nullopt;
else if (newOptions.empty())
return builtinTypes->neverType;
else if (1 == newOptions.size())
return *begin(newOptions);
else
return arena->addType(UnionType{std::vector<TypeId>(begin(newOptions), end(newOptions))});
}
static std::optional<TypeId> unwrapOption(NotNull<BuiltinTypes> builtinTypes, NotNull<TypeArena> arena, const TxnLog& log, TypeId ty)
{
DenseHashSet<TypeId> seen{nullptr};
return unwrapOption(builtinTypes, arena, log, ty, seen);
}
void Unifier::tryUnifyUnionWithType(TypeId subTy, const UnionType* subUnion, TypeId superTy)
{
// Peephole fix: A? <: B? if A <: B
//
// This works around issues that can arise if A or B is free. We do not
// want either of those types to be bound to nil.
if (FFlag::LuauUnifyTwoOptions)
{
if (auto subOption = unwrapOption(builtinTypes, NotNull{types}, log, subTy))
{
if (auto superOption = unwrapOption(builtinTypes, NotNull{types}, log, superTy))
return tryUnify_(*subOption, *superOption);
}
}
// A | B <: T if and only if A <: T and B <: T
bool failed = false;
bool errorsSuppressed = true;
@ -880,7 +795,7 @@ void Unifier::tryUnifyUnionWithType(TypeId subTy, const UnionType* subUnion, Typ
}
}
if (FFlag::DebugLuauDeferredConstraintResolution)
if (FFlag::LuauAlwaysCommitInferencesOfFunctionCalls)
log.concatAsUnion(combineLogsIntoUnion(std::move(logs)), NotNull{types});
else
{
@ -1364,25 +1279,6 @@ void Unifier::tryUnifyNormalizedTypes(
const ClassType* superCtv = get<ClassType>(superClass);
LUAU_ASSERT(superCtv);
if (FFlag::LuauUnifyTwoOptions)
{
if (variance == Invariant)
{
if (subCtv == superCtv)
{
found = true;
/*
* The only way we could care about superNegations is if
* one of them was equal to superCtv. However,
* normalization ensures that this is impossible.
*/
}
else
continue;
}
}
if (isSubclass(subCtv, superCtv))
{
found = true;
@ -2960,7 +2856,6 @@ TxnLog Unifier::combineLogsIntoIntersection(std::vector<TxnLog> logs)
TxnLog Unifier::combineLogsIntoUnion(std::vector<TxnLog> logs)
{
LUAU_ASSERT(FFlag::DebugLuauDeferredConstraintResolution);
TxnLog result(useScopes);
for (TxnLog& log : logs)
result.concatAsUnion(std::move(log), NotNull{types});

View File

@ -1415,7 +1415,7 @@ void AssemblyBuilderX64::commit()
{
LUAU_ASSERT(codePos <= codeEnd);
if (codeEnd - codePos < kMaxInstructionLength)
if (unsigned(codeEnd - codePos) < kMaxInstructionLength)
extend();
}

View File

@ -56,8 +56,10 @@ static void makePagesExecutable(uint8_t* mem, size_t size)
static void flushInstructionCache(uint8_t* mem, size_t size)
{
#if WINAPI_FAMILY_PARTITION(WINAPI_PARTITION_APP | WINAPI_PARTITION_SYSTEM)
if (FlushInstructionCache(GetCurrentProcess(), mem, size) == 0)
LUAU_ASSERT(!"Failed to flush instruction cache");
#endif
}
#else
static uint8_t* allocatePages(size_t size)

View File

@ -766,6 +766,9 @@ void IrLoweringA64::lowerInst(IrInst& inst, uint32_t index, IrBlock& next)
build.ldr(x2, mem(x2, offsetof(global_State, tmname) + intOp(inst.b) * sizeof(TString*)));
build.ldr(x3, mem(rNativeContext, offsetof(NativeContext, luaT_gettm)));
build.blr(x3);
build.cbz(x0, labelOp(inst.c)); // no tag method
inst.regA64 = regs.takeReg(x0, index);
break;
}

View File

@ -639,6 +639,9 @@ void IrLoweringX64::lowerInst(IrInst& inst, uint32_t index, IrBlock& next)
callWrap.call(qword[rNativeContext + offsetof(NativeContext, luaT_gettm)]);
}
build.test(rax, rax);
build.jcc(ConditionX64::Zero, labelOp(inst.c)); // No tag method
inst.regX64 = regs.takeReg(rax, index);
break;
}

View File

@ -146,8 +146,15 @@ struct ConstPropState
void invalidateRegisterRange(int firstReg, int count)
{
for (int i = firstReg; i < firstReg + count && i <= maxReg; ++i)
invalidate(regs[i], /* invalidateTag */ true, /* invalidateValue */ true);
if (count == -1)
{
invalidateRegistersFrom(firstReg);
}
else
{
for (int i = firstReg; i < firstReg + count && i <= maxReg; ++i)
invalidate(regs[i], /* invalidateTag */ true, /* invalidateValue */ true);
}
}
void invalidateCapturedRegisters()
@ -236,9 +243,18 @@ struct ConstPropState
return;
if (uint32_t* prevIdx = valueMap.find(inst))
substitute(function, inst, IrOp{IrOpKind::Inst, *prevIdx});
else
valueMap[inst] = instIdx;
{
const IrInst& prev = function.instructions[*prevIdx];
// Previous load might have been removed as unused
if (prev.useCount != 0)
{
substitute(function, inst, IrOp{IrOpKind::Inst, *prevIdx});
return;
}
}
valueMap[inst] = instIdx;
}
// Vm register load can be replaced by a previous load of the same version of the register
@ -260,23 +276,28 @@ struct ConstPropState
// Check if there is a value that already has this version of the register
if (uint32_t* prevIdx = valueMap.find(versionedLoad))
{
// Previous value might not be linked to a register yet
// For example, it could be a NEW_TABLE stored into a register and we might need to track guards made with this value
if (!instLink.contains(*prevIdx))
createRegLink(*prevIdx, loadInst.a);
const IrInst& prev = function.instructions[*prevIdx];
// Substitute load instructon with the previous value
substitute(function, loadInst, IrOp{IrOpKind::Inst, *prevIdx});
// Previous load might have been removed as unused
if (prev.useCount != 0)
{
// Previous value might not be linked to a register yet
// For example, it could be a NEW_TABLE stored into a register and we might need to track guards made with this value
if (!instLink.contains(*prevIdx))
createRegLink(*prevIdx, loadInst.a);
// Substitute load instructon with the previous value
substitute(function, loadInst, IrOp{IrOpKind::Inst, *prevIdx});
return;
}
}
else
{
uint32_t instIdx = function.getInstIndex(loadInst);
// Record load of this register version for future substitution
valueMap[versionedLoad] = instIdx;
uint32_t instIdx = function.getInstIndex(loadInst);
createRegLink(instIdx, loadInst.a);
}
// Record load of this register version for future substitution
valueMap[versionedLoad] = instIdx;
createRegLink(instIdx, loadInst.a);
}
// VM register loads can use the value that was stored in the same Vm register earlier
@ -456,9 +477,16 @@ static void constPropInInst(ConstPropState& state, IrBuilder& build, IrFunction&
}
if (state.tryGetTag(source) == value)
kill(function, inst);
{
if (FFlag::DebugLuauAbortingChecks)
replace(function, block, index, {IrCmd::CHECK_TAG, inst.a, inst.b, build.undef()});
else
kill(function, inst);
}
else
{
state.saveTag(source, value);
}
}
else
{

View File

@ -14,8 +14,6 @@ inline bool isFlagExperimental(const char* flag)
"LuauInstantiateInSubtyping", // requires some fixes to lua-apps code
"LuauTypecheckTypeguards", // requires some fixes to lua-apps code (CLI-67030)
"LuauTinyControlFlowAnalysis", // waiting for updates to packages depended by internal builtin plugins
"LuauUnifyTwoOptions", // requires some fixes to lua-apps code
// makes sure we always have at least one entry
nullptr,
};

View File

@ -12,8 +12,6 @@
#include <string.h>
#include <stdio.h>
LUAU_FASTFLAGVARIABLE(LuauFixBreakpointLineSearch, false)
static const char* getfuncname(Closure* f);
static int currentpc(lua_State* L, CallInfo* ci)
@ -427,22 +425,11 @@ static int getnextline(Proto* p, int line)
int candidate = luaG_getline(p, i);
if (FFlag::LuauFixBreakpointLineSearch)
{
if (candidate == line)
return line;
if (candidate == line)
return line;
if (candidate > line && (closest == -1 || candidate < closest))
closest = candidate;
}
else
{
if (candidate >= line)
{
closest = candidate;
break;
}
}
if (candidate > line && (closest == -1 || candidate < closest))
closest = candidate;
}
}
@ -451,21 +438,11 @@ static int getnextline(Proto* p, int line)
// Find the closest line number to the intended one.
int candidate = getnextline(p->p[i], line);
if (FFlag::LuauFixBreakpointLineSearch)
{
if (candidate == line)
return line;
if (candidate == line)
return line;
if (candidate > line && (closest == -1 || candidate < closest))
closest = candidate;
}
else
{
if (closest == -1 || (candidate >= line && candidate < closest))
{
closest = candidate;
}
}
if (candidate > line && (closest == -1 || candidate < closest))
closest = candidate;
}
return closest;

View File

@ -561,8 +561,6 @@ TEST_CASE("Debug")
TEST_CASE("Debugger")
{
ScopedFastFlag luauFixBreakpointLineSearch{"LuauFixBreakpointLineSearch", true};
static int breakhits = 0;
static lua_State* interruptedthread = nullptr;
static bool singlestep = false;

View File

@ -1811,6 +1811,30 @@ bb_0:
)");
}
TEST_CASE_FIXTURE(IrBuilderFixture, "VaridicRegisterRangeInvalidation")
{
IrOp block = build.block(IrBlockKind::Internal);
build.beginBlock(block);
build.inst(IrCmd::STORE_TAG, build.vmReg(2), build.constTag(tnumber));
build.inst(IrCmd::FALLBACK_GETVARARGS, build.constUint(0), build.vmReg(1), build.constInt(-1));
build.inst(IrCmd::STORE_TAG, build.vmReg(2), build.constTag(tnumber));
build.inst(IrCmd::RETURN, build.constUint(0));
updateUseCounts(build.function);
constPropInBlockChains(build, true);
CHECK("\n" + toString(build.function, /* includeUseInfo */ false) == R"(
bb_0:
STORE_TAG R2, tnumber
FALLBACK_GETVARARGS 0u, R1, -1i
STORE_TAG R2, tnumber
RETURN 0u
)");
}
TEST_SUITE_END();
TEST_SUITE_BEGIN("Analysis");
@ -2329,4 +2353,60 @@ bb_0:
)");
}
TEST_CASE_FIXTURE(IrBuilderFixture, "NoDeadLoadReuse")
{
IrOp entry = build.block(IrBlockKind::Internal);
build.beginBlock(entry);
IrOp op1 = build.inst(IrCmd::LOAD_DOUBLE, build.vmReg(0));
IrOp op1i = build.inst(IrCmd::NUM_TO_INT, op1);
IrOp res = build.inst(IrCmd::BITAND_UINT, op1i, build.constInt(0));
IrOp resd = build.inst(IrCmd::INT_TO_NUM, res);
IrOp op2 = build.inst(IrCmd::LOAD_DOUBLE, build.vmReg(0));
IrOp sum = build.inst(IrCmd::ADD_NUM, resd, op2);
build.inst(IrCmd::STORE_DOUBLE, build.vmReg(1), sum);
build.inst(IrCmd::RETURN, build.vmReg(1), build.constInt(1));
updateUseCounts(build.function);
constPropInBlockChains(build, true);
CHECK("\n" + toString(build.function, /* includeUseInfo */ false) == R"(
bb_0:
%4 = LOAD_DOUBLE R0
%5 = ADD_NUM 0, %4
STORE_DOUBLE R1, %5
RETURN R1, 1i
)");
}
TEST_CASE_FIXTURE(IrBuilderFixture, "NoDeadValueReuse")
{
IrOp entry = build.block(IrBlockKind::Internal);
build.beginBlock(entry);
IrOp op1 = build.inst(IrCmd::LOAD_DOUBLE, build.vmReg(0));
IrOp op1i = build.inst(IrCmd::NUM_TO_INT, op1);
IrOp res = build.inst(IrCmd::BITAND_UINT, op1i, build.constInt(0));
IrOp op2i = build.inst(IrCmd::NUM_TO_INT, op1);
IrOp sum = build.inst(IrCmd::ADD_INT, res, op2i);
IrOp resd = build.inst(IrCmd::INT_TO_NUM, sum);
build.inst(IrCmd::STORE_DOUBLE, build.vmReg(1), resd);
build.inst(IrCmd::RETURN, build.vmReg(1), build.constInt(1));
updateUseCounts(build.function);
constPropInBlockChains(build, true);
CHECK("\n" + toString(build.function, /* includeUseInfo */ false) == R"(
bb_0:
%0 = LOAD_DOUBLE R0
%3 = NUM_TO_INT %0
%4 = ADD_INT 0i, %3
%5 = INT_TO_NUM %4
STORE_DOUBLE R1, %5
RETURN R1, 1i
)");
}
TEST_SUITE_END();

View File

@ -132,6 +132,8 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "sort_with_predicate")
TEST_CASE_FIXTURE(BuiltinsFixture, "sort_with_bad_predicate")
{
ScopedFastFlag sff{"LuauAlwaysCommitInferencesOfFunctionCalls", true};
CheckResult result = check(R"(
--!strict
local t = {'one', 'two', 'three'}
@ -140,9 +142,9 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "sort_with_bad_predicate")
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(R"(Type '(number, number) -> boolean' could not be converted into '((a, a) -> boolean)?'
CHECK_EQ(R"(Type '(number, number) -> boolean' could not be converted into '((string, string) -> boolean)?'
caused by:
None of the union options are compatible. For example: Type '(number, number) -> boolean' could not be converted into '(a, a) -> boolean'
None of the union options are compatible. For example: Type '(number, number) -> boolean' could not be converted into '(string, string) -> boolean'
caused by:
Argument #1 type is not compatible. Type 'string' could not be converted into 'number')",
toString(result.errors[0]));

View File

@ -367,6 +367,8 @@ b.X = 2 -- real Vector2.X is also read-only
TEST_CASE_FIXTURE(ClassFixture, "detailed_class_unification_error")
{
ScopedFastFlag sff{"LuauAlwaysCommitInferencesOfFunctionCalls", true};
CheckResult result = check(R"(
local function foo(v)
return v.X :: number + string.len(v.Y)
@ -378,10 +380,10 @@ b(a)
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(R"(Type 'Vector2' could not be converted into '{- X: a, Y: string -}'
CHECK_EQ(toString(result.errors[0]), R"(Type 'Vector2' could not be converted into '{- X: number, Y: string -}'
caused by:
Property 'Y' is not compatible. Type 'number' could not be converted into 'string')",
toString(result.errors[0]));
Property 'Y' is not compatible. Type 'number' could not be converted into 'string')");
}
TEST_CASE_FIXTURE(ClassFixture, "class_type_mismatch_with_name_conflict")

View File

@ -2002,4 +2002,75 @@ TEST_CASE_FIXTURE(Fixture, "function_exprs_are_generalized_at_signature_scope_no
CHECK(toString(requireType("foo")) == "<a>(a) -> b");
}
TEST_CASE_FIXTURE(BuiltinsFixture, "param_1_and_2_both_takes_the_same_generic_but_their_arguments_are_incompatible")
{
ScopedFastFlag sff{"LuauAlwaysCommitInferencesOfFunctionCalls", true};
CheckResult result = check(R"(
local function foo<a>(x: a, y: a?)
return x
end
local vec2 = { x = 5, y = 7 }
local ret: number = foo(vec2, { x = 5 })
)");
// In the old solver, this produces a very strange result:
//
// Here, we instantiate `<a>(x: a, y: a?) -> a` with a fresh type `'a` for `a`.
// In argument #1, we unify `vec2` with `'a`.
// This is ok, so we record an equality constraint `'a` with `vec2`.
// In argument #2, we unify `{ x: number }` with `'a?`.
// This fails because `'a` has equality constraint with `vec2`,
// so `{ x: number } <: vec2?`, which is false.
//
// If the unifications were to be committed, then it'd result in the following type error:
//
// Type '{ x: number }' could not be converted into 'vec2?'
// caused by:
// [...] Table type '{ x: number }' not compatible with type 'vec2' because the former is missing field 'y'
//
// However, whenever we check the argument list, if there's an error, we don't commit the unifications, so it actually looks like this:
//
// Type '{ x: number }' could not be converted into 'a?'
// caused by:
// [...] Table type '{ x: number }' not compatible with type 'vec2' because the former is missing field 'y'
//
// Then finally, that generic is left floating free, and since the function returns that generic,
// that free type is then later bound to `number`, which succeeds and mutates the type graph.
// This again changes the type error where `a` becomes bound to `number`.
//
// Type '{ x: number }' could not be converted into 'number?'
// caused by:
// [...] Table type '{ x: number }' not compatible with type 'vec2' because the former is missing field 'y'
//
// Uh oh, that type error is extremely confusing for people who doesn't know how that went down.
// Really, what should happen is we roll each argument incompatibility into a union type, but that needs local type inference.
LUAU_REQUIRE_ERROR_COUNT(2, result);
CHECK_EQ(toString(result.errors[0]), R"(Type '{ x: number }' could not be converted into 'vec2?'
caused by:
None of the union options are compatible. For example: Table type '{ x: number }' not compatible with type 'vec2' because the former is missing field 'y')");
CHECK_EQ(toString(result.errors[1]), "Type 'vec2' could not be converted into 'number'");
}
TEST_CASE_FIXTURE(BuiltinsFixture, "param_1_and_2_both_takes_the_same_generic_but_their_arguments_are_incompatible_2")
{
ScopedFastFlag sff{"LuauAlwaysCommitInferencesOfFunctionCalls", true};
CheckResult result = check(R"(
local function f<a>(x: a, y: a): a
return if math.random() > 0.5 then x else y
end
local z: boolean = f(5, "five")
)");
LUAU_REQUIRE_ERROR_COUNT(2, result);
CHECK_EQ(toString(result.errors[0]), "Type 'string' could not be converted into 'number'");
CHECK_EQ(toString(result.errors[1]), "Type 'number' could not be converted into 'boolean'");
}
TEST_SUITE_END();

View File

@ -500,17 +500,41 @@ TEST_CASE_FIXTURE(Fixture, "intersection_of_tables")
TEST_CASE_FIXTURE(Fixture, "intersection_of_tables_with_top_properties")
{
ScopedFastFlag sff{"LuauUnifyTwoOptions", true};
CheckResult result = check(R"(
local x : { p : number?, q : any } & { p : unknown, q : string? }
local y : { p : number?, q : string? } = x -- OK
local z : { p : string?, q : number? } = x -- Not OK
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(toString(result.errors[0]), "Type '{| p: number?, q: any |} & {| p: unknown, q: string? |}' could not be converted into "
"'{| p: string?, q: number? |}'; none of the intersection parts are compatible");
if (FFlag::DebugLuauDeferredConstraintResolution)
{
LUAU_REQUIRE_ERROR_COUNT(2, result);
CHECK_EQ(toString(result.errors[0]),
"Type '{| p: number?, q: string? |}' could not be converted into '{| p: string?, q: number? |}'\n"
"caused by:\n"
" Property 'p' is not compatible. Type 'number?' could not be converted into 'string?'\n"
"caused by:\n"
" Not all union options are compatible. Type 'number' could not be converted into 'string?'\n"
"caused by:\n"
" None of the union options are compatible. For example: Type 'number' could not be converted into 'string' in an invariant context");
CHECK_EQ(toString(result.errors[1]),
"Type '{| p: number?, q: string? |}' could not be converted into '{| p: string?, q: number? |}'\n"
"caused by:\n"
" Property 'q' is not compatible. Type 'string?' could not be converted into 'number?'\n"
"caused by:\n"
" Not all union options are compatible. Type 'string' could not be converted into 'number?'\n"
"caused by:\n"
" None of the union options are compatible. For example: Type 'string' could not be converted into 'number' in an invariant context");
}
else
{
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(toString(result.errors[0]),
"Type '{| p: number?, q: any |} & {| p: unknown, q: string? |}' could not be converted into '{| p: string?, "
"q: number? |}'; none of the intersection parts are compatible");
}
}
TEST_CASE_FIXTURE(Fixture, "intersection_of_tables_with_never_properties")

View File

@ -522,17 +522,7 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "typecheck_unary_minus_error")
LUAU_REQUIRE_ERROR_COUNT(1, result);
if (FFlag::DebugLuauDeferredConstraintResolution)
{
// Under DCR, this currently functions as a failed overload resolution, and so we can't say
// anything about the result type of the unary minus.
CHECK_EQ("any", toString(requireType("a")));
}
else
{
CHECK_EQ("string", toString(requireType("a")));
}
CHECK_EQ("string", toString(requireType("a")));
TypeMismatch* tm = get<TypeMismatch>(result.errors[0]);
REQUIRE_EQ(*tm->wantedType, *builtinTypes->booleanType);

View File

@ -482,6 +482,41 @@ TEST_CASE_FIXTURE(Fixture, "dcr_can_partially_dispatch_a_constraint")
CHECK("<a>(a, number) -> ()" == toString(requireType("prime_iter")));
}
TEST_CASE_FIXTURE(Fixture, "free_options_cannot_be_unified_together")
{
ScopedFastFlag sff[] = {
{"LuauTransitiveSubtyping", true},
};
TypeArena arena;
TypeId nilType = builtinTypes->nilType;
std::unique_ptr scope = std::make_unique<Scope>(builtinTypes->anyTypePack);
TypeId free1 = arena.addType(FreeType{scope.get()});
TypeId option1 = arena.addType(UnionType{{nilType, free1}});
TypeId free2 = arena.addType(FreeType{scope.get()});
TypeId option2 = arena.addType(UnionType{{nilType, free2}});
InternalErrorReporter iceHandler;
UnifierSharedState sharedState{&iceHandler};
Normalizer normalizer{&arena, builtinTypes, NotNull{&sharedState}};
Unifier u{NotNull{&normalizer}, NotNull{scope.get()}, Location{}, Variance::Covariant};
u.tryUnify(option1, option2);
CHECK(!u.failure);
u.log.commit();
ToStringOptions opts;
CHECK("a?" == toString(option1, opts));
// CHECK("a?" == toString(option2, opts)); // This should hold, but does not.
CHECK("b?" == toString(option2, opts)); // This should not hold.
}
TEST_CASE_FIXTURE(BuiltinsFixture, "for_in_loop_with_zero_iterators")
{
ScopedFastFlag sff{"DebugLuauDeferredConstraintResolution", false};
@ -822,7 +857,6 @@ TEST_CASE_FIXTURE(Fixture, "lookup_prop_of_intersection_containing_unions_of_tab
TEST_CASE_FIXTURE(Fixture, "expected_type_should_be_a_helpful_deduction_guide_for_function_calls")
{
ScopedFastFlag sffs[]{
{"LuauUnifyTwoOptions", true},
{"LuauTypeMismatchInvarianceInError", true},
};
@ -836,22 +870,11 @@ TEST_CASE_FIXTURE(Fixture, "expected_type_should_be_a_helpful_deduction_guide_fo
local x: Ref<number?> = useRef(nil)
)");
if (FFlag::DebugLuauDeferredConstraintResolution)
{
// This is actually wrong! Sort of. It's doing the wrong thing, it's actually asking whether
// `{| val: number? |} <: {| val: nil |}`
// instead of the correct way, which is
// `{| val: nil |} <: {| val: number? |}`
LUAU_REQUIRE_NO_ERRORS(result);
}
else
{
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(toString(result.errors[0]), R"(Type 'Ref<nil>' could not be converted into 'Ref<number?>'
caused by:
Property 'val' is not compatible. Type 'nil' could not be converted into 'number' in an invariant context)");
}
// This is actually wrong! Sort of. It's doing the wrong thing, it's actually asking whether
// `{| val: number? |} <: {| val: nil |}`
// instead of the correct way, which is
// `{| val: nil |} <: {| val: number? |}`
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "floating_generics_should_not_be_allowed")
@ -876,4 +899,116 @@ TEST_CASE_FIXTURE(Fixture, "floating_generics_should_not_be_allowed")
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "free_options_can_be_unified_together")
{
ScopedFastFlag sff[] = {
{"LuauTransitiveSubtyping", true},
};
TypeArena arena;
TypeId nilType = builtinTypes->nilType;
std::unique_ptr scope = std::make_unique<Scope>(builtinTypes->anyTypePack);
TypeId free1 = arena.addType(FreeType{scope.get()});
TypeId option1 = arena.addType(UnionType{{nilType, free1}});
TypeId free2 = arena.addType(FreeType{scope.get()});
TypeId option2 = arena.addType(UnionType{{nilType, free2}});
InternalErrorReporter iceHandler;
UnifierSharedState sharedState{&iceHandler};
Normalizer normalizer{&arena, builtinTypes, NotNull{&sharedState}};
Unifier u{NotNull{&normalizer}, NotNull{scope.get()}, Location{}, Variance::Covariant};
u.tryUnify(option1, option2);
CHECK(!u.failure);
u.log.commit();
ToStringOptions opts;
CHECK("a?" == toString(option1, opts));
CHECK("b?" == toString(option2, opts)); // should be `a?`.
}
TEST_CASE_FIXTURE(Fixture, "unify_more_complex_unions_that_include_nil")
{
CheckResult result = check(R"(
type Record = {prop: (string | boolean)?}
function concatPagination(prop: (string | boolean | nil)?): Record
return {prop = prop}
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "optional_class_instances_are_invariant")
{
ScopedFastFlag sff[] = {
{"LuauTypeMismatchInvarianceInError", true}
};
createSomeClasses(&frontend);
CheckResult result = check(R"(
function foo(ref: {current: Parent?})
end
function bar(ref: {current: Child?})
foo(ref)
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(BuiltinsFixture, "luau-polyfill.Map.entries")
{
fileResolver.source["Module/Map"] = R"(
--!strict
type Object = { [any]: any }
type Array<T> = { [number]: T }
type Table<T, V> = { [T]: V }
type Tuple<T, V> = Array<T | V>
local Map = {}
export type Map<K, V> = {
size: number,
-- method definitions
set: (self: Map<K, V>, K, V) -> Map<K, V>,
get: (self: Map<K, V>, K) -> V | nil,
clear: (self: Map<K, V>) -> (),
delete: (self: Map<K, V>, K) -> boolean,
has: (self: Map<K, V>, K) -> boolean,
keys: (self: Map<K, V>) -> Array<K>,
values: (self: Map<K, V>) -> Array<V>,
entries: (self: Map<K, V>) -> Array<Tuple<K, V>>,
ipairs: (self: Map<K, V>) -> any,
[K]: V,
_map: { [K]: V },
_array: { [number]: K },
}
function Map:entries()
return {}
end
local function coerceToTable(mapLike: Map<any, any> | Table<any, any>): Array<Tuple<any, any>>
local e = mapLike:entries();
return e
end
)";
CheckResult result = frontend.check("Module/Map");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_SUITE_END();

View File

@ -390,8 +390,6 @@ TEST_CASE_FIXTURE(Fixture, "widen_the_supertype_if_it_is_free_and_subtype_has_si
TEST_CASE_FIXTURE(Fixture, "return_type_of_f_is_not_widened")
{
ScopedFastFlag sff{"LuauUnifyTwoOptions", true};
CheckResult result = check(R"(
local function foo(f, x): "hello"? -- anyone there?
return if x == "hi"
@ -403,9 +401,7 @@ TEST_CASE_FIXTURE(Fixture, "return_type_of_f_is_not_widened")
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ(R"("hi")", toString(requireTypeAtPosition({3, 23})));
CHECK_EQ(R"(<a, b...>((string) -> ("hello", b...), a) -> "hello"?)", toString(requireType("foo")));
// This is more accurate but we're not there yet:
CHECK_EQ(R"(<a, b, c...>((string) -> (a, c...), b) -> "hello"?)", toString(requireType("foo")));
// CHECK_EQ(R"(<a, b...>((string) -> ("hello"?, b...), a) -> "hello"?)", toString(requireType("foo")));
}

View File

@ -1518,6 +1518,8 @@ TEST_CASE_FIXTURE(Fixture, "right_table_missing_key2")
TEST_CASE_FIXTURE(Fixture, "casting_unsealed_tables_with_props_into_table_with_indexer")
{
ScopedFastFlag sff{"LuauAlwaysCommitInferencesOfFunctionCalls", true};
CheckResult result = check(R"(
type StringToStringMap = { [string]: string }
local rt: StringToStringMap = { ["foo"] = 1 }
@ -1563,6 +1565,8 @@ TEST_CASE_FIXTURE(Fixture, "casting_tables_with_props_into_table_with_indexer2")
TEST_CASE_FIXTURE(Fixture, "casting_tables_with_props_into_table_with_indexer3")
{
ScopedFastFlag sff{"LuauAlwaysCommitInferencesOfFunctionCalls", true};
CheckResult result = check(R"(
local function foo(a: {[string]: number, a: string}) end
foo({ a = 1 })
@ -1574,7 +1578,7 @@ TEST_CASE_FIXTURE(Fixture, "casting_tables_with_props_into_table_with_indexer3")
TypeMismatch* tm = get<TypeMismatch>(result.errors[0]);
REQUIRE(tm);
CHECK_EQ("{| [string]: number, a: string |}", toString(tm->wantedType, o));
CHECK_EQ("{ a: number }", toString(tm->givenType, o));
CHECK_EQ("{ [string]: number, a: number }", toString(tm->givenType, o));
}
TEST_CASE_FIXTURE(Fixture, "casting_tables_with_props_into_table_with_indexer4")
@ -2383,6 +2387,8 @@ TEST_CASE_FIXTURE(Fixture, "confusing_indexing")
TEST_CASE_FIXTURE(Fixture, "pass_a_union_of_tables_to_a_function_that_requires_a_table")
{
ScopedFastFlag sff{"LuauAlwaysCommitInferencesOfFunctionCalls", true};
CheckResult result = check(R"(
local a: {x: number, y: number, [any]: any} | {y: number}
@ -2396,11 +2402,16 @@ TEST_CASE_FIXTURE(Fixture, "pass_a_union_of_tables_to_a_function_that_requires_a
LUAU_REQUIRE_NO_ERRORS(result);
REQUIRE_EQ("{| [any]: any, x: number, y: number |} | {| y: number |}", toString(requireType("b")));
if (FFlag::DebugLuauDeferredConstraintResolution)
REQUIRE_EQ("{| [any]: any, x: number, y: number |} | {| y: number |}", toString(requireType("b")));
else
REQUIRE_EQ("{- y: number -}", toString(requireType("b")));
}
TEST_CASE_FIXTURE(Fixture, "pass_a_union_of_tables_to_a_function_that_requires_a_table_2")
{
ScopedFastFlag sff{"LuauAlwaysCommitInferencesOfFunctionCalls", true};
CheckResult result = check(R"(
local a: {y: number} | {x: number, y: number, [any]: any}
@ -2414,7 +2425,10 @@ TEST_CASE_FIXTURE(Fixture, "pass_a_union_of_tables_to_a_function_that_requires_a
LUAU_REQUIRE_NO_ERRORS(result);
REQUIRE_EQ("{| [any]: any, x: number, y: number |} | {| y: number |}", toString(requireType("b")));
if (FFlag::DebugLuauDeferredConstraintResolution)
REQUIRE_EQ("{| [any]: any, x: number, y: number |} | {| y: number |}", toString(requireType("b")));
else
REQUIRE_EQ("{- y: number -}", toString(requireType("b")));
}
TEST_CASE_FIXTURE(Fixture, "unifying_tables_shouldnt_uaf1")
@ -3292,6 +3306,8 @@ TEST_CASE_FIXTURE(Fixture, "scalar_is_a_subtype_of_a_compatible_polymorphic_shap
TEST_CASE_FIXTURE(Fixture, "scalar_is_not_a_subtype_of_a_compatible_polymorphic_shape_type")
{
ScopedFastFlag sff{"LuauAlwaysCommitInferencesOfFunctionCalls", true};
CheckResult result = check(R"(
local function f(s)
return s:absolutely_no_scalar_has_this_method()
@ -3308,10 +3324,12 @@ TEST_CASE_FIXTURE(Fixture, "scalar_is_not_a_subtype_of_a_compatible_polymorphic_
caused by:
The former's metatable does not satisfy the requirements. Table type 'typeof(string)' not compatible with type 't1 where t1 = {- absolutely_no_scalar_has_this_method: (t1) -> (a...) -}' because the former is missing field 'absolutely_no_scalar_has_this_method')",
toString(result.errors[0]));
CHECK_EQ(R"(Type '"bar"' could not be converted into 't1 where t1 = {- absolutely_no_scalar_has_this_method: (t1) -> (a...) -}'
caused by:
The former's metatable does not satisfy the requirements. Table type 'typeof(string)' not compatible with type 't1 where t1 = {- absolutely_no_scalar_has_this_method: (t1) -> (a...) -}' because the former is missing field 'absolutely_no_scalar_has_this_method')",
toString(result.errors[1]));
CHECK_EQ(R"(Type '"bar" | "baz"' could not be converted into 't1 where t1 = {- absolutely_no_scalar_has_this_method: (t1) -> (a...) -}'
caused by:
Not all union options are compatible. Type '"bar"' could not be converted into 't1 where t1 = {- absolutely_no_scalar_has_this_method: (t1) -> (a...) -}'

View File

@ -989,10 +989,6 @@ TEST_CASE_FIXTURE(Fixture, "cli_50041_committing_txnlog_in_apollo_client_error")
function Policies:readField(options: ReadFieldOptions)
local _ = self:getStoreFieldName(options)
--[[
Type error:
TypeError { "MainModule", Location { { line = 25, col = 16 }, { line = 25, col = 20 } }, TypeMismatch { Policies, {- getStoreFieldName: (tp1) -> (a, b...) -} } }
]]
foo(self)
end
)");
@ -1006,9 +1002,9 @@ TEST_CASE_FIXTURE(Fixture, "cli_50041_committing_txnlog_in_apollo_client_error")
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(
R"(Type 't1 where t1 = {+ getStoreFieldName: (t1, {| fieldName: string |} & {| from: number? |}) -> (a, b...) +}' could not be converted into 'Policies'
R"(Type 'Policies' from 'MainModule' could not be converted into 'Policies' from 'MainModule'
caused by:
Property 'getStoreFieldName' is not compatible. Type 't1 where t1 = ({+ getStoreFieldName: t1 +}, {| fieldName: string |} & {| from: number? |}) -> (a, b...)' could not be converted into '(Policies, FieldSpecifier) -> string'
Property 'getStoreFieldName' is not compatible. Type '(Policies, FieldSpecifier & {| from: number? |}) -> (a, b...)' could not be converted into '(Policies, FieldSpecifier) -> string'
caused by:
Argument #2 type is not compatible. Type 'FieldSpecifier' could not be converted into 'FieldSpecifier & {| from: number? |}'
caused by:

View File

@ -189,6 +189,8 @@ TEST_CASE_FIXTURE(TryUnifyFixture, "uninhabited_table_sub_anything")
TEST_CASE_FIXTURE(TryUnifyFixture, "members_of_failed_typepack_unification_are_unified_with_errorType")
{
ScopedFastFlag sff{"LuauAlwaysCommitInferencesOfFunctionCalls", true};
CheckResult result = check(R"(
function f(arg: number) end
local a
@ -198,12 +200,14 @@ TEST_CASE_FIXTURE(TryUnifyFixture, "members_of_failed_typepack_unification_are_u
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ("a", toString(requireType("a")));
CHECK_EQ("number", toString(requireType("a")));
CHECK_EQ("*error-type*", toString(requireType("b")));
}
TEST_CASE_FIXTURE(TryUnifyFixture, "result_of_failed_typepack_unification_is_constrained")
{
ScopedFastFlag sff{"LuauAlwaysCommitInferencesOfFunctionCalls", true};
CheckResult result = check(R"(
function f(arg: number) return arg end
local a
@ -213,7 +217,7 @@ TEST_CASE_FIXTURE(TryUnifyFixture, "result_of_failed_typepack_unification_is_con
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ("a", toString(requireType("a")));
CHECK_EQ("number", toString(requireType("a")));
CHECK_EQ("*error-type*", toString(requireType("b")));
CHECK_EQ("number", toString(requireType("c")));
}
@ -442,7 +446,10 @@ TEST_CASE_FIXTURE(TryUnifyFixture, "unifying_two_unions_under_dcr_does_not_creat
const TypeId innerType = arena.freshType(nestedScope.get());
ScopedFastFlag sff{"DebugLuauDeferredConstraintResolution", true};
ScopedFastFlag sffs[]{
{"DebugLuauDeferredConstraintResolution", true},
{"LuauAlwaysCommitInferencesOfFunctionCalls", true},
};
state.enableScopeTests();

View File

@ -789,128 +789,4 @@ TEST_CASE_FIXTURE(Fixture, "lookup_prop_of_intersection_containing_unions")
CHECK("variables" == unknownProp->key);
}
TEST_CASE_FIXTURE(Fixture, "free_options_can_be_unified_together")
{
ScopedFastFlag sff[] = {
{"LuauTransitiveSubtyping", true},
{"LuauUnifyTwoOptions", true}
};
TypeArena arena;
TypeId nilType = builtinTypes->nilType;
std::unique_ptr scope = std::make_unique<Scope>(builtinTypes->anyTypePack);
TypeId free1 = arena.addType(FreeType{scope.get()});
TypeId option1 = arena.addType(UnionType{{nilType, free1}});
TypeId free2 = arena.addType(FreeType{scope.get()});
TypeId option2 = arena.addType(UnionType{{nilType, free2}});
InternalErrorReporter iceHandler;
UnifierSharedState sharedState{&iceHandler};
Normalizer normalizer{&arena, builtinTypes, NotNull{&sharedState}};
Unifier u{NotNull{&normalizer}, NotNull{scope.get()}, Location{}, Variance::Covariant};
u.tryUnify(option1, option2);
CHECK(!u.failure);
u.log.commit();
ToStringOptions opts;
CHECK("a?" == toString(option1, opts));
CHECK("a?" == toString(option2, opts));
}
TEST_CASE_FIXTURE(Fixture, "unify_more_complex_unions_that_include_nil")
{
CheckResult result = check(R"(
type Record = {prop: (string | boolean)?}
function concatPagination(prop: (string | boolean | nil)?): Record
return {prop = prop}
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "optional_class_instances_are_invariant")
{
ScopedFastFlag sff[] = {
{"LuauUnifyTwoOptions", true},
{"LuauTypeMismatchInvarianceInError", true}
};
createSomeClasses(&frontend);
CheckResult result = check(R"(
function foo(ref: {current: Parent?})
end
function bar(ref: {current: Child?})
foo(ref)
end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
// The last line of this error is the most important part. We need to
// communicate that this is an invariant context.
std::string expectedError =
"Type '{| current: Child? |}' could not be converted into '{| current: Parent? |}'\n"
"caused by:\n"
" Property 'current' is not compatible. Type 'Child' could not be converted into 'Parent' in an invariant context"
;
CHECK(expectedError == toString(result.errors[0]));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "luau-polyfill.Map.entries")
{
fileResolver.source["Module/Map"] = R"(
--!strict
type Object = { [any]: any }
type Array<T> = { [number]: T }
type Table<T, V> = { [T]: V }
type Tuple<T, V> = Array<T | V>
local Map = {}
export type Map<K, V> = {
size: number,
-- method definitions
set: (self: Map<K, V>, K, V) -> Map<K, V>,
get: (self: Map<K, V>, K) -> V | nil,
clear: (self: Map<K, V>) -> (),
delete: (self: Map<K, V>, K) -> boolean,
has: (self: Map<K, V>, K) -> boolean,
keys: (self: Map<K, V>) -> Array<K>,
values: (self: Map<K, V>) -> Array<V>,
entries: (self: Map<K, V>) -> Array<Tuple<K, V>>,
ipairs: (self: Map<K, V>) -> any,
[K]: V,
_map: { [K]: V },
_array: { [number]: K },
}
function Map:entries()
return {}
end
local function coerceToTable(mapLike: Map<any, any> | Table<any, any>): Array<Tuple<any, any>>
local e = mapLike:entries();
return e
end
)";
CheckResult result = frontend.check("Module/Map");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_SUITE_END();

View File

@ -25,7 +25,7 @@ local function fuzzfail1(...)
end
end
local function fuzzFail2()
local function fuzzfail2()
local _
do
repeat
@ -35,6 +35,30 @@ local function fuzzFail2()
end
end
assert(pcall(fuzzFail2) == false)
assert(pcall(fuzzfail2) == false)
local function fuzzfail3()
function _(...)
_({_,_,true,},{...,},_,not _)
end
_()
end
assert(pcall(fuzzfail3) == false)
local function fuzzfail4()
local _ = setmetatable({},setmetatable({_=_,},_))
return _(_:_())
end
assert(pcall(fuzzfail4) == false)
local function fuzzfail5()
local _ = bit32.band
_(_(_,0),_)
_(_,_)
end
assert(pcall(fuzzfail5) == false)
return('OK')

View File

@ -11,7 +11,6 @@ BuiltinTests.select_slightly_out_of_range
BuiltinTests.select_way_out_of_range
BuiltinTests.set_metatable_needs_arguments
BuiltinTests.setmetatable_should_not_mutate_persisted_types
BuiltinTests.sort_with_bad_predicate
BuiltinTests.string_format_as_method
BuiltinTests.string_format_correctly_ordered_types
BuiltinTests.string_format_report_all_type_errors_at_correct_positions
@ -37,12 +36,14 @@ GenericsTests.infer_generic_lib_function_function_argument
GenericsTests.instantiated_function_argument_names
GenericsTests.no_stack_overflow_from_quantifying
GenericsTests.self_recursive_instantiated_param
IntersectionTypes.intersection_of_tables_with_top_properties
IntersectionTypes.table_intersection_write_sealed_indirect
IntersectionTypes.table_write_sealed_indirect
ProvisionalTests.assign_table_with_refined_property_with_a_similar_type_is_illegal
ProvisionalTests.do_not_ice_when_trying_to_pick_first_of_generic_type_pack
ProvisionalTests.error_on_eq_metamethod_returning_a_type_other_than_boolean
ProvisionalTests.expected_type_should_be_a_helpful_deduction_guide_for_function_calls
ProvisionalTests.free_options_can_be_unified_together
ProvisionalTests.free_options_cannot_be_unified_together
ProvisionalTests.greedy_inference_with_shared_self_triggers_function_with_no_returns
ProvisionalTests.luau-polyfill.Array.filter
ProvisionalTests.setmetatable_constrains_free_type_into_free_table
@ -150,8 +151,11 @@ TypeInferFunctions.too_few_arguments_variadic_generic2
TypeInferFunctions.too_many_arguments_error_location
TypeInferFunctions.too_many_return_values_in_parentheses
TypeInferFunctions.too_many_return_values_no_function
TypeInferLoops.dcr_iteration_explore_raycast_minimization
TypeInferLoops.for_in_loop_error_on_factory_not_returning_the_right_amount_of_values
TypeInferLoops.loop_iter_metamethod_ok_with_inference
TypeInferLoops.loop_iter_trailing_nil
TypeInferLoops.properly_infer_iteratee_is_a_free_table
TypeInferLoops.unreachable_code_after_infinite_loop
TypeInferModules.do_not_modify_imported_types_5
TypeInferModules.module_type_conflict
@ -164,7 +168,6 @@ TypeInferOperators.cli_38355_recursive_union
TypeInferOperators.compound_assign_mismatch_metatable
TypeInferOperators.disallow_string_and_types_without_metatables_from_arithmetic_binary_ops
TypeInferOperators.luau-polyfill.String.slice
TypeInferOperators.luau_polyfill_is_array
TypeInferOperators.operator_eq_completely_incompatible
TypeInferOperators.typecheck_overloaded_multiply_that_is_an_intersection
TypeInferOperators.typecheck_overloaded_multiply_that_is_an_intersection_on_rhs