From daf79328fc85bae9781239271ff3184153484363 Mon Sep 17 00:00:00 2001 From: Alexander McCord <11488393+alexmccord@users.noreply.github.com> Date: Fri, 31 May 2024 12:18:18 -0700 Subject: [PATCH] Sync to upstream/release/628 (#1278) ### What's new? * Remove a case of unsound `table.move` optimization * Add Luau stack slot reservations that were missing in REPL (fixes #1273) ### New Type Solver * Assignments have been completely reworked to fix a case of cyclic constraint dependency * When indexing, if the fresh type's upper bound already contains a compatible indexer, do not add another upper bound * Distribute type arguments over all type families sans `eq`, `keyof`, `rawkeyof`, and other internal type families * Fix a case where `buffers` component weren't read in two places (fixes #1267) * Fix a case where things that constitutes a strong ref were slightly incorrect * Fix a case where constraint dependencies weren't setup wrt `for ... in` statement ### Native Codegen * Fix an optimization that splits TValue store only when its value and its tag are compatible * Implement a system to plug additional type information for custom host userdata types --- ### Internal Contributors Co-authored-by: Aaron Weiss Co-authored-by: Alexander McCord Co-authored-by: Andy Friesen Co-authored-by: Vighnesh Vijay Co-authored-by: Vyacheslav Egorov --------- Co-authored-by: Aaron Weiss Co-authored-by: Andy Friesen Co-authored-by: Vighnesh Co-authored-by: Aviral Goel Co-authored-by: David Cope Co-authored-by: Lily Brown Co-authored-by: Vyacheslav Egorov --- Analysis/include/Luau/Constraint.h | 77 +-- Analysis/include/Luau/ConstraintGenerator.h | 17 +- Analysis/include/Luau/ConstraintSolver.h | 20 +- Analysis/include/Luau/Simplify.h | 3 + Analysis/include/Luau/TypeFamily.h | 21 +- Analysis/include/Luau/TypeUtils.h | 3 + Analysis/src/Constraint.cpp | 42 +- Analysis/src/ConstraintGenerator.cpp | 415 ++++--------- Analysis/src/ConstraintSolver.cpp | 610 +++++++++++++------- Analysis/src/Simplify.cpp | 11 + Analysis/src/Subtyping.cpp | 1 + Analysis/src/ToString.cpp | 17 +- Analysis/src/TypeChecker2.cpp | 27 + Analysis/src/TypeFamily.cpp | 257 +++++---- Analysis/src/TypeUtils.cpp | 53 ++ CLI/Repl.cpp | 6 + CodeGen/include/Luau/CodeGen.h | 11 +- CodeGen/include/Luau/IrDump.h | 6 +- CodeGen/include/Luau/IrUtils.h | 4 + CodeGen/src/BytecodeAnalysis.cpp | 100 +--- CodeGen/src/CodeGenAssembly.cpp | 136 +++-- CodeGen/src/CodeGenContext.cpp | 24 + CodeGen/src/CodeGenContext.h | 3 + CodeGen/src/CodeGenLower.h | 7 +- CodeGen/src/IrBuilder.cpp | 32 +- CodeGen/src/IrDump.cpp | 79 ++- CodeGen/src/IrTranslation.cpp | 7 +- CodeGen/src/IrUtils.cpp | 10 + CodeGen/src/OptimizeConstProp.cpp | 50 +- Common/include/Luau/Bytecode.h | 8 +- Compiler/include/Luau/BytecodeBuilder.h | 14 + Compiler/include/Luau/Compiler.h | 3 + Compiler/include/luacode.h | 3 + Compiler/src/BytecodeBuilder.cpp | 146 ++++- Compiler/src/Compiler.cpp | 40 +- Compiler/src/Types.cpp | 47 +- Compiler/src/Types.h | 6 +- VM/src/lstate.h | 1 + VM/src/ltablib.cpp | 63 -- VM/src/lvmload.cpp | 104 +++- tests/Compiler.test.cpp | 91 ++- tests/Conformance.test.cpp | 9 +- tests/ConformanceIrHooks.h | 2 + tests/IrBuilder.test.cpp | 58 +- tests/IrLowering.test.cpp | 125 +++- tests/NonStrictTypeChecker.test.cpp | 18 + tests/Repl.test.cpp | 18 + tests/Subtyping.test.cpp | 1 + tests/ToString.test.cpp | 5 +- tests/TypeFamily.test.cpp | 117 +++- tests/TypeInfer.anyerror.test.cpp | 52 +- tests/TypeInfer.functions.test.cpp | 2 +- tests/TypeInfer.loops.test.cpp | 44 ++ tests/TypeInfer.operators.test.cpp | 6 +- tests/TypeInfer.primitives.test.cpp | 10 + tests/TypeInfer.singletons.test.cpp | 14 + tests/TypeInfer.tables.test.cpp | 39 +- tests/TypeInfer.typestates.test.cpp | 1 + tests/TypeInfer.unionTypes.test.cpp | 8 +- tests/conformance/move.lua | 37 +- tests/conformance/native.lua | 29 + tools/faillist.txt | 29 +- 62 files changed, 1986 insertions(+), 1213 deletions(-) diff --git a/Analysis/include/Luau/Constraint.h b/Analysis/include/Luau/Constraint.h index ec281ae3..77810516 100644 --- a/Analysis/include/Luau/Constraint.h +++ b/Analysis/include/Luau/Constraint.h @@ -179,23 +179,6 @@ struct HasPropConstraint bool suppressSimplification = false; }; -// result ~ setProp subjectType ["prop", "prop2", ...] propType -// -// If the subject is a table or table-like thing that already has the named -// property chain, we unify propType with that existing property type. -// -// If the subject is a free table, we augment it in place. -// -// If the subject is an unsealed table, result is an augmented table that -// includes that new prop. -struct SetPropConstraint -{ - TypeId resultType; - TypeId subjectType; - std::vector path; - TypeId propType; -}; - // resultType ~ hasIndexer subjectType indexType // // If the subject type is a table or table-like thing that supports indexing, @@ -209,16 +192,37 @@ struct HasIndexerConstraint TypeId indexType; }; -// result ~ setIndexer subjectType indexType propType -// -// If the subject is a table or table-like thing that already has an indexer, -// unify its indexType and propType with those from this constraint. -// -// If the table is a free or unsealed table, we augment it with a new indexer. -struct SetIndexerConstraint +struct AssignConstraint { - TypeId subjectType; + TypeId lhsType; + TypeId rhsType; +}; + +// assign lhsType propName rhsType +// +// Assign a value of type rhsType into the named property of lhsType. + +struct AssignPropConstraint +{ + TypeId lhsType; + std::string propName; + TypeId rhsType; + + /// The canonical write type of the property. It is _solely_ used to + /// populate astTypes during constraint resolution. Nothing should ever + /// block on it. + TypeId propType; +}; + +struct AssignIndexConstraint +{ + TypeId lhsType; TypeId indexType; + TypeId rhsType; + + /// The canonical write type of the property. It is _solely_ used to + /// populate astTypes during constraint resolution. Nothing should ever + /// block on it. TypeId propType; }; @@ -230,25 +234,6 @@ struct UnpackConstraint { TypePackId resultPack; TypePackId sourcePack; - - // UnpackConstraint is sometimes used to resolve the types of assignments. - // When this is the case, any LocalTypes in resultPack can have their - // domains extended by the corresponding type from sourcePack. - bool resultIsLValue = false; -}; - -// resultType ~ unpack sourceType -// -// The same as UnpackConstraint, but specialized for a pair of types as opposed to packs. -struct Unpack1Constraint -{ - TypeId resultType; - TypeId sourceType; - - // UnpackConstraint is sometimes used to resolve the types of assignments. - // When this is the case, any LocalTypes in resultPack can have their - // domains extended by the corresponding type from sourcePack. - bool resultIsLValue = false; }; // ty ~ reduce ty @@ -268,8 +253,8 @@ struct ReducePackConstraint }; using ConstraintV = Variant; + TypeAliasExpansionConstraint, FunctionCallConstraint, FunctionCheckConstraint, PrimitiveTypeConstraint, HasPropConstraint, HasIndexerConstraint, + AssignConstraint, AssignPropConstraint, AssignIndexConstraint, UnpackConstraint, ReduceConstraint, ReducePackConstraint, EqualityConstraint>; struct Constraint { diff --git a/Analysis/include/Luau/ConstraintGenerator.h b/Analysis/include/Luau/ConstraintGenerator.h index ed5e17e2..3e1861ea 100644 --- a/Analysis/include/Luau/ConstraintGenerator.h +++ b/Analysis/include/Luau/ConstraintGenerator.h @@ -254,18 +254,11 @@ private: Inference check(const ScopePtr& scope, AstExprTable* expr, std::optional expectedType); std::tuple checkBinary(const ScopePtr& scope, AstExprBinary* binary, std::optional expectedType); - struct LValueBounds - { - std::optional annotationTy; - std::optional assignedTy; - }; - - LValueBounds checkLValue(const ScopePtr& scope, AstExpr* expr); - LValueBounds checkLValue(const ScopePtr& scope, AstExprLocal* local); - LValueBounds checkLValue(const ScopePtr& scope, AstExprGlobal* global); - LValueBounds checkLValue(const ScopePtr& scope, AstExprIndexName* indexName); - LValueBounds checkLValue(const ScopePtr& scope, AstExprIndexExpr* indexExpr); - LValueBounds updateProperty(const ScopePtr& scope, AstExpr* expr); + void visitLValue(const ScopePtr& scope, AstExpr* expr, TypeId rhsType); + void visitLValue(const ScopePtr& scope, AstExprLocal* local, TypeId rhsType); + void visitLValue(const ScopePtr& scope, AstExprGlobal* global, TypeId rhsType); + void visitLValue(const ScopePtr& scope, AstExprIndexName* indexName, TypeId rhsType); + void visitLValue(const ScopePtr& scope, AstExprIndexExpr* indexExpr, TypeId rhsType); struct FunctionSignature { diff --git a/Analysis/include/Luau/ConstraintSolver.h b/Analysis/include/Luau/ConstraintSolver.h index 031da67b..58361dde 100644 --- a/Analysis/include/Luau/ConstraintSolver.h +++ b/Analysis/include/Luau/ConstraintSolver.h @@ -134,7 +134,6 @@ struct ConstraintSolver bool tryDispatch(const FunctionCheckConstraint& c, NotNull constraint); bool tryDispatch(const PrimitiveTypeConstraint& c, NotNull constraint); bool tryDispatch(const HasPropConstraint& c, NotNull constraint); - bool tryDispatch(const SetPropConstraint& c, NotNull constraint); bool tryDispatchHasIndexer( int& recursionDepth, NotNull constraint, TypeId subjectType, TypeId indexType, TypeId resultType, Set& seen); @@ -142,11 +141,13 @@ struct ConstraintSolver std::pair> tryDispatchSetIndexer( NotNull constraint, TypeId subjectType, TypeId indexType, TypeId propType, bool expandFreeTypeBounds); - bool tryDispatch(const SetIndexerConstraint& c, NotNull constraint, bool force); - bool tryDispatchUnpack1(NotNull constraint, TypeId resultType, TypeId sourceType, bool resultIsLValue); + bool tryDispatch(const AssignConstraint& c, NotNull constraint); + bool tryDispatch(const AssignPropConstraint& c, NotNull constraint); + bool tryDispatch(const AssignIndexConstraint& c, NotNull constraint); + + bool tryDispatchUnpack1(NotNull constraint, TypeId resultType, TypeId sourceType); bool tryDispatch(const UnpackConstraint& c, NotNull constraint); - bool tryDispatch(const Unpack1Constraint& c, NotNull constraint); bool tryDispatch(const ReduceConstraint& c, NotNull constraint, bool force); bool tryDispatch(const ReducePackConstraint& c, NotNull constraint, bool force); @@ -165,6 +166,17 @@ struct ConstraintSolver std::pair, std::optional> lookupTableProp(NotNull constraint, TypeId subjectType, const std::string& propName, ValueContext context, bool inConditional, bool suppressSimplification, DenseHashSet& seen); + /** + * Generate constraints to unpack the types of srcTypes and assign each + * value to the corresponding LocalType in destTypes. + * + * @param destTypes A finite TypePack comprised of LocalTypes. + * @param srcTypes A TypePack that represents rvalues to be assigned. + * @returns The underlying UnpackConstraint. There's a bit of code in + * iteration that needs to pass blocks on to this constraint. + */ + NotNull unpackAndAssign(TypePackId destTypes, TypePackId srcTypes, NotNull constraint); + void block(NotNull target, NotNull constraint); /** * Block a constraint on the resolution of a Type. diff --git a/Analysis/include/Luau/Simplify.h b/Analysis/include/Luau/Simplify.h index 10f27d4e..5b363e96 100644 --- a/Analysis/include/Luau/Simplify.h +++ b/Analysis/include/Luau/Simplify.h @@ -5,6 +5,7 @@ #include "Luau/DenseHash.h" #include "Luau/NotNull.h" #include "Luau/TypeFwd.h" +#include namespace Luau { @@ -19,6 +20,8 @@ struct SimplifyResult }; SimplifyResult simplifyIntersection(NotNull builtinTypes, NotNull arena, TypeId ty, TypeId discriminant); +SimplifyResult simplifyIntersection(NotNull builtinTypes, NotNull arena, std::set parts); + SimplifyResult simplifyUnion(NotNull builtinTypes, NotNull arena, TypeId ty, TypeId discriminant); enum class Relation diff --git a/Analysis/include/Luau/TypeFamily.h b/Analysis/include/Luau/TypeFamily.h index fa418e17..5b72a370 100644 --- a/Analysis/include/Luau/TypeFamily.h +++ b/Analysis/include/Luau/TypeFamily.h @@ -6,7 +6,6 @@ #include "Luau/NotNull.h" #include "Luau/TypeCheckLimits.h" #include "Luau/TypeFwd.h" -#include "Luau/Variant.h" #include #include @@ -19,22 +18,6 @@ struct TypeArena; struct TxnLog; class Normalizer; -struct TypeFamilyQueue -{ - NotNull> queuedTys; - NotNull> queuedTps; - - void add(TypeId instanceTy); - void add(TypePackId instanceTp); - - template - void add(const std::vector& ts) - { - for (const T& t : ts) - enqueue(t); - } -}; - struct TypeFamilyContext { NotNull arena; @@ -99,8 +82,8 @@ struct TypeFamilyReductionResult }; template -using ReducerFunction = std::function( - T, NotNull, const std::vector&, const std::vector&, NotNull)>; +using ReducerFunction = std::function(T, const std::vector&, const std::vector&, + NotNull)>; /// Represents a type function that may be applied to map a series of types and /// type packs to a single output type. diff --git a/Analysis/include/Luau/TypeUtils.h b/Analysis/include/Luau/TypeUtils.h index 81c8a5ca..c8ee99e9 100644 --- a/Analysis/include/Luau/TypeUtils.h +++ b/Analysis/include/Luau/TypeUtils.h @@ -55,6 +55,9 @@ struct InConditionalContext using ScopePtr = std::shared_ptr; +std::optional findTableProperty( + NotNull builtinTypes, ErrorVec& errors, TypeId ty, const std::string& name, Location location); + std::optional findMetatableEntry( NotNull builtinTypes, ErrorVec& errors, TypeId type, const std::string& entry, Location location); std::optional findTablePropertyRespectingMeta( diff --git a/Analysis/src/Constraint.cpp b/Analysis/src/Constraint.cpp index 4d1c35e0..bd31beff 100644 --- a/Analysis/src/Constraint.cpp +++ b/Analysis/src/Constraint.cpp @@ -56,6 +56,11 @@ bool isReferenceCountedType(const TypeId typ) DenseHashSet Constraint::getMaybeMutatedFreeTypes() const { + // For the purpose of this function and reference counting in general, we are only considering + // mutations that affect the _bounds_ of the free type, and not something that may bind the free + // type itself to a new type. As such, `ReduceConstraint` and `GeneralizationConstraint` have no + // contribution to the output set here. + DenseHashSet types{{}}; ReferenceCountInitializer rci{&types}; @@ -74,11 +79,6 @@ DenseHashSet Constraint::getMaybeMutatedFreeTypes() const rci.traverse(psc->subPack); rci.traverse(psc->superPack); } - else if (auto gc = get(*this)) - { - rci.traverse(gc->generalizedType); - // `GeneralizationConstraints` should not mutate `sourceType` or `interiorTypes`. - } else if (auto itc = get(*this)) { rci.traverse(itc->variables); @@ -101,36 +101,32 @@ DenseHashSet Constraint::getMaybeMutatedFreeTypes() const rci.traverse(hpc->resultType); // `HasPropConstraints` should not mutate `subjectType`. } - else if (auto spc = get(*this)) - { - rci.traverse(spc->resultType); - // `SetPropConstraints` should not mutate `subjectType` or `propType`. - // TODO: is this true? it "unifies" with `propType`, so maybe mutates that one too? - } else if (auto hic = get(*this)) { rci.traverse(hic->resultType); // `HasIndexerConstraint` should not mutate `subjectType` or `indexType`. } - else if (auto sic = get(*this)) + else if (auto ac = get(*this)) { - rci.traverse(sic->propType); - // `SetIndexerConstraints` should not mutate `subjectType` or `indexType`. + rci.traverse(ac->lhsType); + rci.traverse(ac->rhsType); + } + else if (auto apc = get(*this)) + { + rci.traverse(apc->lhsType); + rci.traverse(apc->rhsType); + } + else if (auto aic = get(*this)) + { + rci.traverse(aic->lhsType); + rci.traverse(aic->indexType); + rci.traverse(aic->rhsType); } else if (auto uc = get(*this)) { rci.traverse(uc->resultPack); // `UnpackConstraint` should not mutate `sourcePack`. } - else if (auto u1c = get(*this)) - { - rci.traverse(u1c->resultType); - // `Unpack1Constraint` should not mutate `sourceType`. - } - else if (auto rc = get(*this)) - { - rci.traverse(rc->ty); - } else if (auto rpc = get(*this)) { rci.traverse(rpc->tp); diff --git a/Analysis/src/ConstraintGenerator.cpp b/Analysis/src/ConstraintGenerator.cpp index cbd027bb..12648eb0 100644 --- a/Analysis/src/ConstraintGenerator.cpp +++ b/Analysis/src/ConstraintGenerator.cpp @@ -310,7 +310,7 @@ std::optional ConstraintGenerator::lookup(const ScopePtr& scope, Locatio std::optional ty = lookup(scope, location, operand, /*prototype*/ false); if (!ty) { - ty = arena->addType(BlockedType{}); + ty = arena->addType(LocalType{builtinTypes->neverType}); rootScope->lvalueTypes[operand] = *ty; } @@ -739,12 +739,28 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatLocal* stat if (hasAnnotation) { + for (size_t i = 0; i < statLocal->vars.size; ++i) + addConstraint(scope, statLocal->location, AssignConstraint{assignees[i], annotatedTypes[i]}); + TypePackId annotatedPack = arena->addTypePack(std::move(annotatedTypes)); - addConstraint(scope, statLocal->location, UnpackConstraint{arena->addTypePack(std::move(assignees)), annotatedPack, /*resultIsLValue*/ true}); addConstraint(scope, statLocal->location, PackSubtypeConstraint{rvaluePack, annotatedPack}); } else - addConstraint(scope, statLocal->location, UnpackConstraint{arena->addTypePack(std::move(assignees)), rvaluePack, /*resultIsLValue*/ true}); + { + std::vector valueTypes; + valueTypes.reserve(statLocal->vars.size); + + for (size_t i = 0; i < statLocal->vars.size; ++i) + valueTypes.push_back(arena->addType(BlockedType{})); + + auto uc = addConstraint(scope, statLocal->location, UnpackConstraint{arena->addTypePack(valueTypes), rvaluePack}); + + for (size_t i = 0; i < statLocal->vars.size; ++i) + { + getMutable(valueTypes[i])->setOwner(uc); + addConstraint(scope, statLocal->location, AssignConstraint{assignees[i], valueTypes[i]}); + } + } if (statLocal->vars.size == 1 && statLocal->values.size == 1 && firstValueType && scope.get() == rootScope && !hasAnnotation) { @@ -837,7 +853,6 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatFor* for_) ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatForIn* forIn) { ScopePtr loopScope = childScope(forIn, scope); - TypePackId iterator = checkPack(scope, forIn->values).tp; std::vector variableTypes; @@ -862,10 +877,17 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatForIn* forI } TypePackId variablePack = arena->addTypePack(std::move(variableTypes)); - addConstraint( + auto iterable = addConstraint( loopScope, getLocation(forIn->values), IterableConstraint{iterator, variablePack, forIn->values.data[0], &module->astForInNextTypes}); + Checkpoint start = checkpoint(this); visit(loopScope, forIn->body); + Checkpoint end = checkpoint(this); + + // This iter constraint must dispatch first. + forEachConstraint(start, end, this, [&iterable](const ConstraintPtr& runLater) { + runLater->dependencies.push_back(iterable); + }); return ControlFlow::None; } @@ -957,67 +979,63 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatFunction* f // Name could be AstStatLocal, AstStatGlobal, AstStatIndexName. // With or without self - TypeId generalizedType = arena->addType(BlockedType{}); Checkpoint start = checkpoint(this); FunctionSignature sig = checkFunctionSignature(scope, function->func, /* expectedType */ std::nullopt, function->name->location); bool sigFullyDefined = !hasFreeType(sig.signature); + checkFunctionBody(sig.bodyScope, function->func); + Checkpoint end = checkpoint(this); + + TypeId generalizedType = arena->addType(BlockedType{}); if (sigFullyDefined) emplaceType(asMutable(generalizedType), sig.signature); + else + { + const ScopePtr& constraintScope = sig.signatureScope ? sig.signatureScope : sig.bodyScope; - DenseHashSet excludeList{nullptr}; + NotNull c = addConstraint(constraintScope, function->name->location, GeneralizationConstraint{generalizedType, sig.signature}); + getMutable(generalizedType)->setOwner(c); + + Constraint* previous = nullptr; + forEachConstraint(start, end, this, [&c, &previous](const ConstraintPtr& constraint) { + c->dependencies.push_back(NotNull{constraint.get()}); + + if (auto psc = get(*constraint); psc && psc->returns) + { + if (previous) + constraint->dependencies.push_back(NotNull{previous}); + + previous = constraint.get(); + } + }); + } DefId def = dfg->getDef(function->name); std::optional existingFunctionTy = follow(lookup(scope, function->name->location, def)); - if (get(existingFunctionTy) && sigFullyDefined) - emplaceType(asMutable(*existingFunctionTy), sig.signature); - if (AstExprLocal* localName = function->name->as()) { - if (existingFunctionTy) - { - addConstraint(scope, function->name->location, SubtypeConstraint{generalizedType, *existingFunctionTy}); - - Symbol sym{localName->local}; - scope->bindings[sym].typeId = generalizedType; - } - else - scope->bindings[localName->local] = Binding{generalizedType, localName->location}; + visitLValue(scope, localName, generalizedType); scope->bindings[localName->local] = Binding{sig.signature, localName->location}; scope->lvalueTypes[def] = sig.signature; - scope->rvalueRefinements[def] = sig.signature; } else if (AstExprGlobal* globalName = function->name->as()) { if (!existingFunctionTy) ice->ice("prepopulateGlobalScope did not populate a global name", globalName->location); - if (!sigFullyDefined) - generalizedType = *existingFunctionTy; + // Sketchy: We're specifically looking for BlockedTypes that were + // initially created by ConstraintGenerator::prepopulateGlobalScope. + if (auto bt = get(*existingFunctionTy); bt && nullptr == bt->getOwner()) + emplaceType(asMutable(*existingFunctionTy), generalizedType); scope->bindings[globalName->name] = Binding{sig.signature, globalName->location}; scope->lvalueTypes[def] = sig.signature; - scope->rvalueRefinements[def] = sig.signature; } else if (AstExprIndexName* indexName = function->name->as()) { - Checkpoint check1 = checkpoint(this); - auto [_, lvalueType] = checkLValue(scope, indexName); - Checkpoint check2 = checkpoint(this); - - forEachConstraint(check1, check2, this, [&excludeList](const ConstraintPtr& c) { - excludeList.insert(c.get()); - }); - - // TODO figure out how to populate the location field of the table Property. - - if (lvalueType && *lvalueType != generalizedType) - { - LUAU_ASSERT(get(lvalueType)); - emplaceType(asMutable(*lvalueType), generalizedType); - } + visitLValue(scope, indexName, generalizedType); } else if (AstExprError* err = function->name->as()) { @@ -1029,48 +1047,6 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatFunction* f scope->rvalueRefinements[def] = generalizedType; - checkFunctionBody(sig.bodyScope, function->func); - Checkpoint end = checkpoint(this); - - if (!sigFullyDefined) - { - NotNull constraintScope{sig.signatureScope ? sig.signatureScope.get() : sig.bodyScope.get()}; - std::unique_ptr c = - std::make_unique(constraintScope, function->name->location, GeneralizationConstraint{generalizedType, sig.signature}); - - Constraint* previous = nullptr; - forEachConstraint(start, end, this, [&c, &excludeList, &previous](const ConstraintPtr& constraint) { - if (!excludeList.contains(constraint.get())) - c->dependencies.push_back(NotNull{constraint.get()}); - - if (auto psc = get(*constraint); psc && psc->returns) - { - if (previous) - constraint->dependencies.push_back(NotNull{previous}); - - previous = constraint.get(); - } - }); - - - // We need to check if the blocked type has no owner here because - // if a function is defined twice anywhere in the program like: - // `function f() end` and then later like `function f() end` - // Then there will be exactly one definition in the scope for it because it's a global - // (this is the same as writing f = function() end) - // Therefore, when we visit() the multiple different expression of this global variable - // They will all be aliased to the same blocked type, which means we can create multiple constraints - // for the same blocked type. - if (auto blocked = getMutable(generalizedType); blocked && !blocked->getOwner()) - blocked->setOwner(addConstraint(scope, std::move(c))); - } - - if (BlockedType* bt = getMutable(follow(existingFunctionTy)); bt && !bt->getOwner()) - { - auto uc = addConstraint(scope, function->name->location, Unpack1Constraint{*existingFunctionTy, generalizedType}); - bt->setOwner(uc); - } - return ControlFlow::None; } @@ -1124,38 +1100,20 @@ static void bindFreeType(TypeId a, TypeId b) ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatAssign* assign) { - std::vector upperBounds; - upperBounds.reserve(assign->vars.size); - - std::vector typeStates; - typeStates.reserve(assign->vars.size); - - Checkpoint lvalueBeginCheckpoint = checkpoint(this); - - for (AstExpr* lvalue : assign->vars) - { - auto [upperBound, typeState] = checkLValue(scope, lvalue); - upperBounds.push_back(upperBound.value_or(builtinTypes->unknownType)); - typeStates.push_back(typeState.value_or(builtinTypes->unknownType)); - } - - Checkpoint lvalueEndCheckpoint = checkpoint(this); - TypePackId resultPack = checkPack(scope, assign->values).tp; - auto uc = addConstraint(scope, assign->location, UnpackConstraint{arena->addTypePack(typeStates), resultPack, /*resultIsLValue*/ true}); - forEachConstraint(lvalueBeginCheckpoint, lvalueEndCheckpoint, this, [uc](const ConstraintPtr& constraint) { - uc->dependencies.push_back(NotNull{constraint.get()}); - }); - auto psc = addConstraint(scope, assign->location, PackSubtypeConstraint{resultPack, arena->addTypePack(std::move(upperBounds))}); - psc->dependencies.push_back(uc); + std::vector valueTypes; + valueTypes.reserve(assign->vars.size); - for (TypeId assignee : typeStates) + for (size_t i = 0; i < assign->vars.size; ++i) + valueTypes.push_back(arena->addType(BlockedType{})); + + auto uc = addConstraint(scope, assign->location, UnpackConstraint{arena->addTypePack(valueTypes), resultPack}); + + for (size_t i = 0; i < assign->vars.size; ++i) { - auto blocked = getMutable(assignee); - - if (blocked && !blocked->getOwner()) - blocked->setOwner(uc); + getMutable(valueTypes[i])->setOwner(uc); + visitLValue(scope, assign->vars.data[i], valueTypes[i]); } return ControlFlow::None; @@ -1166,24 +1124,7 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatCompoundAss AstExprBinary binop = AstExprBinary{assign->location, assign->op, assign->var, assign->value}; TypeId resultTy = check(scope, &binop).ty; - auto [upperBound, typeState] = checkLValue(scope, assign->var); - - Constraint* sc = nullptr; - if (upperBound) - sc = addConstraint(scope, assign->location, SubtypeConstraint{resultTy, *upperBound}); - - if (typeState) - { - NotNull uc = addConstraint(scope, assign->location, Unpack1Constraint{*typeState, resultTy, /*resultIsLValue=*/true}); - if (auto blocked = getMutable(*typeState); blocked && !blocked->getOwner()) - blocked->setOwner(uc); - - if (sc) - uc->dependencies.push_back(NotNull{sc}); - } - - DefId def = dfg->getDef(assign->var); - scope->lvalueTypes[def] = resultTy; + visitLValue(scope, assign->var, resultTy); return ControlFlow::None; } @@ -1897,7 +1838,8 @@ Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprGlobal* globa return Inference{builtinTypes->errorRecoveryType()}; } -Inference ConstraintGenerator::checkIndexName(const ScopePtr& scope, const RefinementKey* key, AstExpr* indexee, const std::string& index, Location indexLocation) +Inference ConstraintGenerator::checkIndexName( + const ScopePtr& scope, const RefinementKey* key, AstExpr* indexee, const std::string& index, Location indexLocation) { TypeId obj = check(scope, indexee).ty; TypeId result = arena->addType(BlockedType{}); @@ -2272,26 +2214,25 @@ std::tuple ConstraintGenerator::checkBinary( } } -ConstraintGenerator::LValueBounds ConstraintGenerator::checkLValue(const ScopePtr& scope, AstExpr* expr) +void ConstraintGenerator::visitLValue(const ScopePtr& scope, AstExpr* expr, TypeId rhsType) { - if (auto local = expr->as()) - return checkLValue(scope, local); - else if (auto global = expr->as()) - return checkLValue(scope, global); - else if (auto indexName = expr->as()) - return checkLValue(scope, indexName); - else if (auto indexExpr = expr->as()) - return checkLValue(scope, indexExpr); - else if (auto error = expr->as()) + if (auto e = expr->as()) + visitLValue(scope, e, rhsType); + else if (auto e = expr->as()) + visitLValue(scope, e, rhsType); + else if (auto e = expr->as()) + visitLValue(scope, e, rhsType); + else if (auto e = expr->as()) + visitLValue(scope, e, rhsType); + else if (auto e = expr->as()) { - check(scope, error); - return {builtinTypes->errorRecoveryType(), builtinTypes->errorRecoveryType()}; + // Nothing? } else - ice->ice("checkLValue is inexhaustive"); + ice->ice("Unexpected lvalue expression", expr->location); } -ConstraintGenerator::LValueBounds ConstraintGenerator::checkLValue(const ScopePtr& scope, AstExprLocal* local) +void ConstraintGenerator::visitLValue(const ScopePtr& scope, AstExprLocal* local, TypeId rhsType) { std::optional annotatedTy = scope->lookup(local->local); LUAU_ASSERT(annotatedTy); @@ -2332,186 +2273,53 @@ ConstraintGenerator::LValueBounds ConstraintGenerator::checkLValue(const ScopePt scope->lvalueTypes[defId] = *ty; } - // TODO: Need to clip this, but this requires more code to be reworked first before we can clip this. - std::optional assignedTy = arena->addType(BlockedType{}); - - auto unpackC = addConstraint(scope, local->location, Unpack1Constraint{*ty, *assignedTy, /*resultIsLValue*/ true}); - - if (auto blocked = get(*ty)) - { - if (blocked->getOwner()) - unpackC->dependencies.push_back(NotNull{blocked->getOwner()}); - else if (auto blocked = getMutable(*ty)) - blocked->setOwner(unpackC); - } - recordInferredBinding(local->local, *ty); - return {annotatedTy, assignedTy}; + if (annotatedTy) + addConstraint(scope, local->location, SubtypeConstraint{rhsType, *annotatedTy}); + addConstraint(scope, local->location, AssignConstraint{*ty, rhsType}); } -ConstraintGenerator::LValueBounds ConstraintGenerator::checkLValue(const ScopePtr& scope, AstExprGlobal* global) +void ConstraintGenerator::visitLValue(const ScopePtr& scope, AstExprGlobal* global, TypeId rhsType) { std::optional annotatedTy = scope->lookup(Symbol{global->name}); if (annotatedTy) { DefId def = dfg->getDef(global); - TypeId assignedTy = arena->addType(BlockedType{}); - rootScope->lvalueTypes[def] = assignedTy; - return {annotatedTy, assignedTy}; + rootScope->lvalueTypes[def] = rhsType; + + addConstraint(scope, global->location, SubtypeConstraint{rhsType, *annotatedTy}); + addConstraint(scope, global->location, AssignConstraint{*annotatedTy, rhsType}); } - else - return {annotatedTy, std::nullopt}; } -ConstraintGenerator::LValueBounds ConstraintGenerator::checkLValue(const ScopePtr& scope, AstExprIndexName* indexName) +void ConstraintGenerator::visitLValue(const ScopePtr& scope, AstExprIndexName* expr, TypeId rhsType) { - return updateProperty(scope, indexName); + TypeId lhsTy = check(scope, expr->expr).ty; + TypeId propTy = arena->addType(BlockedType{}); + module->astTypes[expr] = propTy; + addConstraint(scope, expr->location, AssignPropConstraint{lhsTy, expr->index.value, rhsType, propTy}); } -ConstraintGenerator::LValueBounds ConstraintGenerator::checkLValue(const ScopePtr& scope, AstExprIndexExpr* indexExpr) +void ConstraintGenerator::visitLValue(const ScopePtr& scope, AstExprIndexExpr* expr, TypeId rhsType) { - return updateProperty(scope, indexExpr); -} - -/** - * This function is mostly about identifying properties that are being inserted into unsealed tables. - * - * If expr has the form name.a.b.c - */ -ConstraintGenerator::LValueBounds ConstraintGenerator::updateProperty(const ScopePtr& scope, AstExpr* expr) -{ - // There are a bunch of cases where we realize that this is not the kind of - // assignment that potentially changes the shape of a table. When we - // encounter them, we call this to fall back and do the "usual thing." - auto fallback = [&]() -> LValueBounds { - TypeId resTy = check(scope, expr).ty; - return {resTy, std::nullopt}; - }; - - LUAU_ASSERT(expr->is() || expr->is()); - - if (auto indexExpr = expr->as(); indexExpr && !indexExpr->index->is()) + if (auto constantString = expr->index->as()) { - // An indexer is only interesting in an lvalue-ey way if it is at the - // tail of an expression. - // - // If the indexer is not at the tail, then we are not interested in - // augmenting the lhs data structure with a new indexer. Constraint - // generation can treat it as an ordinary lvalue. - // - // eg - // - // a.b.c[1] = 44 -- lvalue - // a.b[4].c = 2 -- rvalue + TypeId lhsTy = check(scope, expr->expr).ty; + TypeId propTy = arena->addType(BlockedType{}); + module->astTypes[expr] = propTy; + module->astTypes[expr->index] = builtinTypes->stringType; // FIXME? Singleton strings exist. + std::string propName{constantString->value.data, constantString->value.size}; + addConstraint(scope, expr->location, AssignPropConstraint{lhsTy, std::move(propName), rhsType, propTy}); - TypeId subjectType = check(scope, indexExpr->expr).ty; - TypeId indexType = check(scope, indexExpr->index).ty; - TypeId assignedTy = arena->addType(BlockedType{}); - auto sic = addConstraint(scope, expr->location, SetIndexerConstraint{subjectType, indexType, assignedTy}); - getMutable(assignedTy)->setOwner(sic); - - module->astTypes[expr] = assignedTy; - - return {assignedTy, assignedTy}; + return; } - Symbol sym; - const Def* def = nullptr; - std::vector segments; - std::vector exprs; - - AstExpr* e = expr; - while (e) - { - if (auto global = e->as()) - { - sym = global->name; - def = dfg->getDef(global); - break; - } - else if (auto local = e->as()) - { - sym = local->local; - def = dfg->getDef(local); - break; - } - else if (auto indexName = e->as()) - { - segments.push_back(indexName->index.value); - exprs.push_back(e); - e = indexName->expr; - } - else if (auto indexExpr = e->as()) - { - if (auto strIndex = indexExpr->index->as()) - { - // We need to populate astTypes for the index value. - check(scope, indexExpr->index); - - segments.push_back(std::string(strIndex->value.data, strIndex->value.size)); - exprs.push_back(e); - e = indexExpr->expr; - } - else - { - return fallback(); - } - } - else - { - return fallback(); - } - } - - LUAU_ASSERT(!segments.empty()); - - std::reverse(begin(segments), end(segments)); - std::reverse(begin(exprs), end(exprs)); - - LUAU_ASSERT(def); - std::optional> lookupResult = scope->lookupEx(NotNull{def}); - if (!lookupResult) - return fallback(); - - const auto [subjectType, subjectScope] = *lookupResult; - - std::vector segmentStrings(begin(segments), end(segments)); - - TypeId updatedType = arena->addType(BlockedType{}); - TypeId assignedTy = arena->addType(BlockedType{}); - auto setC = addConstraint(scope, expr->location, SetPropConstraint{updatedType, subjectType, std::move(segmentStrings), assignedTy}); - getMutable(updatedType)->setOwner(setC); - - TypeId prevSegmentTy = updatedType; - for (size_t i = 0; i < segments.size(); ++i) - { - TypeId segmentTy = arena->addType(BlockedType{}); - module->astTypes[exprs[i]] = segmentTy; - ValueContext ctx = i == segments.size() - 1 ? ValueContext::LValue : ValueContext::RValue; - auto hasC = addConstraint(scope, expr->location, HasPropConstraint{segmentTy, prevSegmentTy, segments[i], ctx, inConditional(typeContext)}); - getMutable(segmentTy)->setOwner(hasC); - setC->dependencies.push_back(hasC); - prevSegmentTy = segmentTy; - } - - module->astTypes[expr] = prevSegmentTy; - module->astTypes[e] = updatedType; - - if (!subjectType->persistent) - { - subjectScope->bindings[sym].typeId = updatedType; - - // This can fail if the user is erroneously trying to augment a builtin - // table like os or string. - if (auto key = dfg->getRefinementKey(e)) - { - subjectScope->lvalueTypes[key->def] = updatedType; - subjectScope->rvalueRefinements[key->def] = updatedType; - } - } - - return {assignedTy, assignedTy}; + TypeId lhsTy = check(scope, expr->expr).ty; + TypeId indexTy = check(scope, expr->index).ty; + TypeId propTy = arena->addType(BlockedType{}); + module->astTypes[expr] = propTy; + addConstraint(scope, expr->location, AssignIndexConstraint{lhsTy, indexTy, rhsType, propTy}); } Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprTable* expr, std::optional expectedType) @@ -3194,6 +3002,11 @@ void ConstraintGenerator::reportCodeTooComplex(Location location) TypeId ConstraintGenerator::makeUnion(const ScopePtr& scope, Location location, TypeId lhs, TypeId rhs) { + if (get(follow(lhs))) + return rhs; + if (get(follow(rhs))) + return lhs; + TypeId resultType = createTypeFamilyInstance(kBuiltinTypeFamilies.unionFamily, {lhs, rhs}, {}, scope, location); return resultType; diff --git a/Analysis/src/ConstraintSolver.cpp b/Analysis/src/ConstraintSolver.cpp index e35ddf0e..b0f27911 100644 --- a/Analysis/src/ConstraintSolver.cpp +++ b/Analysis/src/ConstraintSolver.cpp @@ -206,6 +206,12 @@ static std::pair, std::vector> saturateArguments saturatedPackArguments.push_back(builtinTypes->errorRecoveryTypePack()); } + for (TypeId& arg : saturatedTypeArguments) + arg = follow(arg); + + for (TypePackId& pack : saturatedPackArguments) + pack = follow(pack); + // At this point, these two conditions should be true. If they aren't we // will run into access violations. LUAU_ASSERT(saturatedTypeArguments.size() == fn.typeParams.size()); @@ -407,11 +413,17 @@ void ConstraintSolver::run() // decrement the referenced free types for this constraint if we dispatched successfully! for (auto ty : c->getMaybeMutatedFreeTypes()) { - // this is a little weird, but because we're only counting free types in subtyping constraints, - // some constraints (like unpack) might actually produce _more_ references to a free type. size_t& refCount = unresolvedConstraints[ty]; if (refCount > 0) refCount -= 1; + + // We have two constraints that are designed to wait for the + // refCount on a free type to be equal to 1: the + // PrimitiveTypeConstraint and ReduceConstraint. We + // therefore wake any constraint waiting for a free type's + // refcount to be 1 or 0. + if (refCount <= 1) + unblock(ty, Location{}); } if (logger) @@ -518,15 +530,15 @@ bool ConstraintSolver::tryDispatch(NotNull constraint, bool fo success = tryDispatch(*fcc, constraint); else if (auto hpc = get(*constraint)) success = tryDispatch(*hpc, constraint); - else if (auto spc = get(*constraint)) - success = tryDispatch(*spc, constraint); else if (auto spc = get(*constraint)) success = tryDispatch(*spc, constraint); - else if (auto spc = get(*constraint)) - success = tryDispatch(*spc, constraint, force); - else if (auto uc = get(*constraint)) + else if (auto uc = get(*constraint)) success = tryDispatch(*uc, constraint); - else if (auto uc = get(*constraint)) + else if (auto uc = get(*constraint)) + success = tryDispatch(*uc, constraint); + else if (auto uc = get(*constraint)) + success = tryDispatch(*uc, constraint); + else if (auto uc = get(*constraint)) success = tryDispatch(*uc, constraint); else if (auto rc = get(*constraint)) success = tryDispatch(*rc, constraint, force); @@ -688,7 +700,18 @@ bool ConstraintSolver::tryDispatch(const IterableConstraint& c, NotNull(tableTy)->indexer = TableIndexer{keyTy, valueTy}; pushConstraint(constraint->scope, constraint->location, SubtypeConstraint{nextTy, tableTy}); - pushConstraint(constraint->scope, constraint->location, UnpackConstraint{c.variables, arena->addTypePack({keyTy, valueTy}), /*resultIsLValue=*/true}); + + auto it = begin(c.variables); + auto endIt = end(c.variables); + + if (it != endIt) + { + pushConstraint(constraint->scope, constraint->location, AssignConstraint{*it, keyTy}); + ++it; + } + if (it != endIt) + pushConstraint(constraint->scope, constraint->location, AssignConstraint{*it, valueTy}); + return true; } @@ -915,7 +938,17 @@ bool ConstraintSolver::tryDispatch(const TypeAliasExpansionConstraint& c, NotNul // Type function application will happily give us the exact same type if // there are e.g. generic saturatedTypeArguments that go unused. const TableType* tfTable = getTableType(tf->type); - bool needsClone = follow(tf->type) == target || (tfTable != nullptr && tfTable == getTableType(target)); + + //clang-format off + bool needsClone = + follow(tf->type) == target || + (tfTable != nullptr && tfTable == getTableType(target)) || + std::any_of(typeArguments.begin(), typeArguments.end(), [&](const auto& other) { + return other == target; + } + ); + //clang-format on + // Only tables have the properties we're trying to set. TableType* ttv = getMutableTableType(target); @@ -1291,158 +1324,6 @@ bool ConstraintSolver::tryDispatch(const HasPropConstraint& c, NotNull(ty); - return ttv && ttv->state == TableState::Unsealed; -} - -/** - * Given a path into a set of nested unsealed tables `ty`, insert a new property `replaceTy` as the leaf-most property. - * - * Fails and does nothing if every table along the way is not unsealed. - * - * Mutates the innermost table type in-place. - */ -static void updateTheTableType( - NotNull builtinTypes, NotNull arena, TypeId ty, const std::vector& path, TypeId replaceTy) -{ - if (path.empty()) - return; - - // First walk the path and ensure that it's unsealed tables all the way - // to the end. - { - TypeId t = ty; - for (size_t i = 0; i < path.size() - 1; ++i) - { - if (!isUnsealedTable(t)) - return; - - const TableType* tbl = get(t); - auto it = tbl->props.find(path[i]); - if (it == tbl->props.end()) - return; - - t = follow(it->second.type()); - } - - // The last path segment should not be a property of the table at all. - // We are not changing property types. We are only admitting this one - // new property to be appended. - if (!isUnsealedTable(t)) - return; - const TableType* tbl = get(t); - if (0 != tbl->props.count(path.back())) - return; - } - - TypeId t = ty; - ErrorVec dummy; - - for (size_t i = 0; i < path.size() - 1; ++i) - { - t = follow(t); - auto propTy = findTablePropertyRespectingMeta(builtinTypes, dummy, t, path[i], ValueContext::LValue, Location{}); - dummy.clear(); - - if (!propTy) - return; - - t = *propTy; - } - - const std::string& lastSegment = path.back(); - - t = follow(t); - TableType* tt = getMutable(t); - if (auto mt = get(t)) - tt = getMutable(mt->table); - - if (!tt) - return; - - tt->props[lastSegment].setType(replaceTy); -} - -bool ConstraintSolver::tryDispatch(const SetPropConstraint& c, NotNull constraint) -{ - TypeId subjectType = follow(c.subjectType); - const TypeId propType = follow(c.propType); - - if (isBlocked(subjectType)) - return block(subjectType, constraint); - - std::optional existingPropType = subjectType; - - LUAU_ASSERT(!c.path.empty()); - if (c.path.empty()) - return false; - - for (size_t i = 0; i < c.path.size(); ++i) - { - const std::string& segment = c.path[i]; - if (!existingPropType) - break; - - ValueContext ctx = i == c.path.size() - 1 ? ValueContext::LValue : ValueContext::RValue; - - auto [blocked, result] = lookupTableProp(constraint, *existingPropType, segment, ctx); - if (!blocked.empty()) - { - for (TypeId blocked : blocked) - block(blocked, constraint); - return false; - } - - existingPropType = result; - } - - auto bind = [&](TypeId a, TypeId b) { - bindBlockedType(a, b, subjectType, constraint); - }; - - if (existingPropType) - { - unify(constraint, propType, *existingPropType); - unify(constraint, *existingPropType, propType); - bind(c.resultType, c.subjectType); - unblock(c.resultType, constraint->location); - return true; - } - - const TypeId originalSubjectType = subjectType; - - if (auto mt = get(subjectType)) - subjectType = follow(mt->table); - - if (get(subjectType)) - return false; - else if (auto ttv = getMutable(subjectType)) - { - if (ttv->state == TableState::Free) - { - LUAU_ASSERT(!subjectType->persistent); - - ttv->props[c.path[0]] = Property{propType}; - bind(c.resultType, subjectType); - unblock(c.resultType, constraint->location); - return true; - } - else if (ttv->state == TableState::Unsealed) - { - LUAU_ASSERT(!subjectType->persistent); - - updateTheTableType(builtinTypes, NotNull{arena}, subjectType, c.path, propType); - } - } - - bind(c.resultType, originalSubjectType); - unblock(c.resultType, constraint->location); - return true; -} - bool ConstraintSolver::tryDispatchHasIndexer( int& recursionDepth, NotNull constraint, TypeId subjectType, TypeId indexType, TypeId resultType, Set& seen) { @@ -1460,6 +1341,13 @@ bool ConstraintSolver::tryDispatchHasIndexer( if (auto ft = get(subjectType)) { + if (auto tbl = get(follow(ft->upperBound)); tbl && tbl->indexer) + { + unify(constraint, indexType, tbl->indexer->indexType); + bindBlockedType(resultType, tbl->indexer->indexResultType, subjectType, constraint); + return true; + } + FreeType freeResult{ft->scope, builtinTypes->neverType, builtinTypes->unknownType}; emplaceType(asMutable(resultType), freeResult); @@ -1708,33 +1596,20 @@ std::pair> ConstraintSolver::tryDispatchSetIndexer( return {true, std::nullopt}; } -bool ConstraintSolver::tryDispatch(const SetIndexerConstraint& c, NotNull constraint, bool force) +bool ConstraintSolver::tryDispatch(const AssignConstraint& c, NotNull constraint) { - TypeId subjectType = follow(c.subjectType); - if (isBlocked(subjectType)) - return block(subjectType, constraint); + const TypeId lhsTy = follow(c.lhsType); + const TypeId rhsTy = follow(c.rhsType); - auto [dispatched, resultTy] = tryDispatchSetIndexer(constraint, subjectType, c.indexType, c.propType, /*expandFreeTypeBounds=*/true); - if (dispatched) - { - bindBlockedType(c.propType, resultTy.value_or(builtinTypes->errorRecoveryType()), subjectType, constraint); - unblock(c.propType, constraint->location); - } - - return dispatched; -} - -bool ConstraintSolver::tryDispatchUnpack1(NotNull constraint, TypeId resultTy, TypeId srcTy, bool resultIsLValue) -{ - resultTy = follow(resultTy); - LUAU_ASSERT(canMutate(resultTy, constraint)); + if (!get(lhsTy) && isBlocked(lhsTy)) + return block(lhsTy, constraint); auto tryExpand = [&](TypeId ty) { LocalType* lt = getMutable(ty); - if (!lt || !resultIsLValue) + if (!lt) return; - lt->domain = simplifyUnion(builtinTypes, arena, lt->domain, srcTy).result; + lt->domain = simplifyUnion(builtinTypes, arena, lt->domain, rhsTy).result; LUAU_ASSERT(lt->blockCount > 0); --lt->blockCount; @@ -1745,11 +1620,289 @@ bool ConstraintSolver::tryDispatchUnpack1(NotNull constraint, } }; - if (auto ut = get(resultTy)) - std::for_each(begin(ut), end(ut), tryExpand); - else if (get(resultTy)) - tryExpand(resultTy); - else if (get(resultTy)) + if (auto ut = get(lhsTy)) + { + // FIXME: I suspect there's a bug here where lhsTy is a union that contains no LocalTypes. + for (TypeId t : ut) + tryExpand(t); + } + else if (get(lhsTy)) + tryExpand(lhsTy); + else + unify(constraint, rhsTy, lhsTy); + + unblock(lhsTy, constraint->location); + + return true; +} + +bool ConstraintSolver::tryDispatch(const AssignPropConstraint& c, NotNull constraint) +{ + TypeId lhsType = follow(c.lhsType); + const std::string& propName = c.propName; + const TypeId rhsType = follow(c.rhsType); + + if (isBlocked(lhsType)) + return block(lhsType, constraint); + + // 1. lhsType is a class that already has the prop + // 2. lhsType is a table that already has the prop (or a union or + // intersection that has the prop in aggregate) + // 3. lhsType has a metatable that already has the prop + // 4. lhsType is an unsealed table that does not have the prop, but has a + // string indexer + // 5. lhsType is an unsealed table that does not have the prop or a string + // indexer + + // Important: In every codepath through this function, the type `c.propType` + // must be bound to something, even if it's just the errorType. + + if (auto lhsClass = get(lhsType)) + { + const Property* prop = lookupClassProp(lhsClass, propName); + if (!prop || !prop->writeTy.has_value()) + return true; + + emplaceType(asMutable(c.propType), *prop->writeTy); + unify(constraint, rhsType, *prop->writeTy); + return true; + } + + if (auto lhsFree = getMutable(lhsType)) + { + if (get(lhsFree->upperBound) || get(lhsFree->upperBound)) + lhsType = lhsFree->upperBound; + else + { + TypeId newUpperBound = arena->addType(TableType{TableState::Free, TypeLevel{}, constraint->scope}); + TableType* upperTable = getMutable(newUpperBound); + LUAU_ASSERT(upperTable); + + upperTable->props[c.propName] = rhsType; + + // Food for thought: Could we block if simplification encounters a blocked type? + lhsFree->upperBound = simplifyIntersection(builtinTypes, arena, lhsFree->upperBound, newUpperBound).result; + + emplaceType(asMutable(c.propType), rhsType); + return true; + } + } + + // Handle the case that lhsType is a table that already has the property or + // a matching indexer. This also handles unions and intersections. + const auto [blocked, maybeTy] = lookupTableProp(constraint, lhsType, propName, ValueContext::LValue); + if (!blocked.empty()) + { + for (TypeId t : blocked) + block(t, constraint); + return false; + } + + if (maybeTy) + { + const TypeId propTy = *maybeTy; + emplaceType(asMutable(c.propType), propTy); + unify(constraint, rhsType, propTy); + return true; + } + + if (auto lhsMeta = get(lhsType)) + lhsType = follow(lhsMeta->table); + + // Handle the case where the lhs type is a table that does not have the + // named property. It could be a table with a string indexer, or an unsealed + // or free table that can grow. + if (auto lhsTable = getMutable(lhsType)) + { + if (auto it = lhsTable->props.find(propName); it != lhsTable->props.end()) + { + Property& prop = it->second; + + if (prop.writeTy.has_value()) + { + emplaceType(asMutable(c.propType), *prop.writeTy); + unify(constraint, rhsType, *prop.writeTy); + return true; + } + else + { + LUAU_ASSERT(prop.isReadOnly()); + if (lhsTable->state == TableState::Unsealed || lhsTable->state == TableState::Free) + { + prop.writeTy = prop.readTy; + emplaceType(asMutable(c.propType), *prop.writeTy); + unify(constraint, rhsType, *prop.writeTy); + return true; + } + else + { + emplaceType(asMutable(c.propType), builtinTypes->errorType); + return true; + } + } + } + + if (lhsTable->indexer && maybeString(lhsTable->indexer->indexType)) + { + emplaceType(asMutable(c.propType), rhsType); + unify(constraint, rhsType, lhsTable->indexer->indexResultType); + return true; + } + + if (lhsTable->state == TableState::Unsealed || lhsTable->state == TableState::Free) + { + emplaceType(asMutable(c.propType), rhsType); + lhsTable->props[propName] = Property::rw(rhsType); + return true; + } + } + + emplaceType(asMutable(c.propType), builtinTypes->errorType); + + return true; +} + +bool ConstraintSolver::tryDispatch(const AssignIndexConstraint& c, NotNull constraint) +{ + const TypeId lhsType = follow(c.lhsType); + const TypeId indexType = follow(c.indexType); + const TypeId rhsType = follow(c.rhsType); + + if (isBlocked(lhsType)) + return block(lhsType, constraint); + + // 0. lhsType could be an intersection or union. + // 1. lhsType is a class with an indexer + // 2. lhsType is a table with an indexer, or it has a metatable that has an indexer + // 3. lhsType is a free or unsealed table and can grow an indexer + + // Important: In every codepath through this function, the type `c.propType` + // must be bound to something, even if it's just the errorType. + + auto tableStuff = [&](TableType* lhsTable) -> std::optional { + if (lhsTable->indexer) + { + unify(constraint, indexType, lhsTable->indexer->indexType); + unify(constraint, rhsType, lhsTable->indexer->indexResultType); + emplaceType(asMutable(c.propType), lhsTable->indexer->indexResultType); + return true; + } + + if (lhsTable->state == TableState::Unsealed || lhsTable->state == TableState::Free) + { + lhsTable->indexer = TableIndexer{indexType, rhsType}; + emplaceType(asMutable(c.propType), rhsType); + return true; + } + + return {}; + }; + + if (auto lhsFree = getMutable(lhsType)) + { + if (auto lhsTable = getMutable(lhsFree->upperBound)) + { + if (auto res = tableStuff(lhsTable)) + return *res; + } + + TypeId newUpperBound = + arena->addType(TableType{/*props*/ {}, TableIndexer{indexType, rhsType}, TypeLevel{}, constraint->scope, TableState::Free}); + const TableType* newTable = get(newUpperBound); + LUAU_ASSERT(newTable); + + unify(constraint, lhsType, newUpperBound); + + LUAU_ASSERT(newTable->indexer); + emplaceType(asMutable(c.propType), newTable->indexer->indexResultType); + return true; + } + + if (auto lhsTable = getMutable(lhsType)) + { + std::optional res = tableStuff(lhsTable); + if (res.has_value()) + return *res; + } + + if (auto lhsClass = get(lhsType)) + { + while (true) + { + if (lhsClass->indexer) + { + unify(constraint, indexType, lhsClass->indexer->indexType); + unify(constraint, rhsType, lhsClass->indexer->indexResultType); + emplaceType(asMutable(c.propType), lhsClass->indexer->indexResultType); + return true; + } + + if (lhsClass->parent) + lhsClass = get(lhsClass->parent); + else + break; + } + return true; + } + + if (auto lhsIntersection = getMutable(lhsType)) + { + std::set parts; + + for (TypeId t : lhsIntersection) + { + if (auto tbl = getMutable(follow(t))) + { + if (tbl->indexer) + { + unify(constraint, indexType, tbl->indexer->indexType); + parts.insert(tbl->indexer->indexResultType); + } + + if (tbl->state == TableState::Unsealed || tbl->state == TableState::Free) + { + tbl->indexer = TableIndexer{indexType, rhsType}; + parts.insert(rhsType); + } + } + else if (auto cls = get(follow(t))) + { + while (true) + { + if (cls->indexer) + { + unify(constraint, indexType, cls->indexer->indexType); + parts.insert(cls->indexer->indexResultType); + break; + } + + if (cls->parent) + cls = get(cls->parent); + else + break; + } + } + } + + TypeId res = simplifyIntersection(builtinTypes, arena, std::move(parts)).result; + + unify(constraint, rhsType, res); + } + + // Other types do not support index assignment. + emplaceType(asMutable(c.propType), builtinTypes->errorType); + + return true; +} + +bool ConstraintSolver::tryDispatchUnpack1(NotNull constraint, TypeId resultTy, TypeId srcTy) +{ + resultTy = follow(resultTy); + LUAU_ASSERT(canMutate(resultTy, constraint)); + + LUAU_ASSERT(get(resultTy)); + + if (get(resultTy)) { if (follow(srcTy) == resultTy) { @@ -1765,10 +1918,7 @@ bool ConstraintSolver::tryDispatchUnpack1(NotNull constraint, bindBlockedType(resultTy, srcTy, srcTy, constraint); } else - { - LUAU_ASSERT(resultIsLValue); unify(constraint, srcTy, resultTy); - } unblock(resultTy, constraint->location); return true; @@ -1804,7 +1954,7 @@ bool ConstraintSolver::tryDispatch(const UnpackConstraint& c, NotNull(resultTy); c.resultIsLValue && lt) - { - lt->domain = simplifyUnion(builtinTypes, arena, lt->domain, builtinTypes->nilType).result; - LUAU_ASSERT(0 <= lt->blockCount); - --lt->blockCount; - - if (0 == lt->blockCount) - { - shiftReferences(resultTy, lt->domain); - emplaceType(asMutable(resultTy), lt->domain); - } - } - else if (get(resultTy) || get(resultTy)) + if (get(resultTy) || get(resultTy)) { emplaceType(asMutable(resultTy), builtinTypes->nilType); unblock(resultTy, constraint->location); @@ -1842,11 +1980,6 @@ bool ConstraintSolver::tryDispatch(const UnpackConstraint& c, NotNull constraint) -{ - return tryDispatchUnpack1(constraint, c.resultType, c.sourceType, c.resultIsLValue); -} - bool ConstraintSolver::tryDispatch(const ReduceConstraint& c, NotNull constraint, bool force) { TypeId ty = follow(c.ty); @@ -1942,13 +2075,23 @@ bool ConstraintSolver::tryDispatchIterableTable(TypeId iteratorTy, const Iterabl getMutable(tableTy)->indexer = TableIndexer{keyTy, valueTy}; pushConstraint(constraint->scope, constraint->location, SubtypeConstraint{iteratorTy, tableTy}); - pushConstraint(constraint->scope, constraint->location, UnpackConstraint{c.variables, arena->addTypePack({keyTy, valueTy}), /*resultIsLValue=*/true}); + + auto it = begin(c.variables); + auto endIt = end(c.variables); + if (it != endIt) + { + pushConstraint(constraint->scope, constraint->location, AssignConstraint{*it, keyTy}); + ++it; + } + if (it != endIt) + pushConstraint(constraint->scope, constraint->location, AssignConstraint{*it, valueTy}); + return true; } auto unpack = [&](TypeId ty) { - TypePackId variadic = arena->addTypePack(VariadicTypePack{ty}); - pushConstraint(constraint->scope, constraint->location, UnpackConstraint{c.variables, variadic, /* resultIsLValue */ true}); + for (TypeId varTy : c.variables) + pushConstraint(constraint->scope, constraint->location, AssignConstraint{varTy, ty}); }; if (get(iteratorTy)) @@ -2043,10 +2186,7 @@ bool ConstraintSolver::tryDispatchIterableTable(TypeId iteratorTy, const Iterabl // If nextFn is nullptr, then the iterator function has an improper signature. if (nextFn) - { - const TypePackId nextRetPack = nextFn->retTypes; - pushConstraint(constraint->scope, constraint->location, UnpackConstraint{c.variables, nextRetPack, /* resultIsLValue=*/true}); - } + unpackAndAssign(c.variables, nextFn->retTypes, constraint); return true; } @@ -2119,12 +2259,37 @@ bool ConstraintSolver::tryDispatchIterableFunction( modifiedNextRetHead.push_back(*it); TypePackId modifiedNextRetPack = arena->addTypePack(std::move(modifiedNextRetHead), it.tail()); - auto psc = pushConstraint(constraint->scope, constraint->location, UnpackConstraint{c.variables, modifiedNextRetPack, /* resultIsLValue */ true}); - inheritBlocks(constraint, psc); + + auto unpackConstraint = unpackAndAssign(c.variables, modifiedNextRetPack, constraint); + + inheritBlocks(constraint, unpackConstraint); return true; } +NotNull ConstraintSolver::unpackAndAssign(TypePackId destTypes, TypePackId srcTypes, NotNull constraint) +{ + std::vector unpackedTys; + for (TypeId _ty : destTypes) + { + (void) _ty; + unpackedTys.push_back(arena->addType(BlockedType{})); + } + + TypePackId unpackedTp = arena->addTypePack(TypePack{unpackedTys}); + auto unpackConstraint = pushConstraint(constraint->scope, constraint->location, UnpackConstraint{unpackedTp, srcTypes}); + + size_t i = 0; + for (TypeId varTy : destTypes) + { + pushConstraint(constraint->scope, constraint->location, AssignConstraint{varTy, unpackedTys[i]}); + getMutable(unpackedTys[i])->setOwner(unpackConstraint); + ++i; + } + + return unpackConstraint; +} + std::pair, std::optional> ConstraintSolver::lookupTableProp(NotNull constraint, TypeId subjectType, const std::string& propName, ValueContext context, bool inConditional, bool suppressSimplification) { @@ -2759,8 +2924,13 @@ std::optional ConstraintSolver::generalizeFreeType(NotNull scope, if (get(t)) { auto refCount = unresolvedConstraints.find(t); - if (!refCount || *refCount > 1) + if (refCount && *refCount > 0) return {}; + + // if no reference count is present, then that means the only constraints referring to + // this free type need only for it to be generalized. in principle, this means we could + // have actually never generated the free type in the first place, but we couldn't know + // that until all constraint generation is complete. } return generalize(NotNull{arena}, builtinTypes, scope, type); @@ -2769,7 +2939,7 @@ std::optional ConstraintSolver::generalizeFreeType(NotNull scope, bool ConstraintSolver::hasUnresolvedConstraints(TypeId ty) { if (auto refCount = unresolvedConstraints.find(ty)) - return *refCount > 1; + return *refCount > 0; return false; } diff --git a/Analysis/src/Simplify.cpp b/Analysis/src/Simplify.cpp index ca78d54d..dae7b2d2 100644 --- a/Analysis/src/Simplify.cpp +++ b/Analysis/src/Simplify.cpp @@ -1368,6 +1368,17 @@ SimplifyResult simplifyIntersection(NotNull builtinTypes, NotNull< return SimplifyResult{res, std::move(s.blockedTypes)}; } +SimplifyResult simplifyIntersection(NotNull builtinTypes, NotNull arena, std::set parts) +{ + LUAU_ASSERT(FFlag::DebugLuauDeferredConstraintResolution); + + TypeSimplifier s{builtinTypes, arena}; + + TypeId res = s.intersectFromParts(std::move(parts)); + + return SimplifyResult{res, std::move(s.blockedTypes)}; +} + SimplifyResult simplifyUnion(NotNull builtinTypes, NotNull arena, TypeId left, TypeId right) { LUAU_ASSERT(FFlag::DebugLuauDeferredConstraintResolution); diff --git a/Analysis/src/Subtyping.cpp b/Analysis/src/Subtyping.cpp index f2d51b31..040c3fc6 100644 --- a/Analysis/src/Subtyping.cpp +++ b/Analysis/src/Subtyping.cpp @@ -1438,6 +1438,7 @@ SubtypingResult Subtyping::isCovariantWith( result.andAlso(isCovariantWith(env, subNorm->strings, superNorm->strings)); result.andAlso(isCovariantWith(env, subNorm->strings, superNorm->tables)); result.andAlso(isCovariantWith(env, subNorm->threads, superNorm->threads)); + result.andAlso(isCovariantWith(env, subNorm->buffers, superNorm->buffers)); result.andAlso(isCovariantWith(env, subNorm->tables, superNorm->tables)); result.andAlso(isCovariantWith(env, subNorm->functions, superNorm->functions)); // isCovariantWith(subNorm->tyvars, superNorm->tyvars); diff --git a/Analysis/src/ToString.cpp b/Analysis/src/ToString.cpp index e3ee2252..4e81a870 100644 --- a/Analysis/src/ToString.cpp +++ b/Analysis/src/ToString.cpp @@ -1787,23 +1787,18 @@ std::string toString(const Constraint& constraint, ToStringOptions& opts) { return tos(c.resultType) + " ~ hasProp " + tos(c.subjectType) + ", \"" + c.prop + "\" ctx=" + std::to_string(int(c.context)); } - else if constexpr (std::is_same_v) - { - const std::string pathStr = c.path.size() == 1 ? "\"" + c.path[0] + "\"" : "[\"" + join(c.path, "\", \"") + "\"]"; - return tos(c.resultType) + " ~ setProp " + tos(c.subjectType) + ", " + pathStr + " " + tos(c.propType); - } else if constexpr (std::is_same_v) { return tos(c.resultType) + " ~ hasIndexer " + tos(c.subjectType) + " " + tos(c.indexType); } - else if constexpr (std::is_same_v) - { - return "setIndexer " + tos(c.subjectType) + " [ " + tos(c.indexType) + " ] " + tos(c.propType); - } + else if constexpr (std::is_same_v) + return "assign " + tos(c.lhsType) + " " + tos(c.rhsType); + else if constexpr (std::is_same_v) + return "assignProp " + tos(c.lhsType) + " " + c.propName + " " + tos(c.rhsType); + else if constexpr (std::is_same_v) + return "assignIndex " + tos(c.lhsType) + " " + tos(c.indexType) + " " + tos(c.rhsType); else if constexpr (std::is_same_v) return tos(c.resultPack) + " ~ ...unpack " + tos(c.sourcePack); - else if constexpr (std::is_same_v) - return tos(c.resultType) + " ~ unpack " + tos(c.sourceType); else if constexpr (std::is_same_v) return "reduce " + tos(c.ty); else if constexpr (std::is_same_v) diff --git a/Analysis/src/TypeChecker2.cpp b/Analysis/src/TypeChecker2.cpp index 37e0f039..5ffeb951 100644 --- a/Analysis/src/TypeChecker2.cpp +++ b/Analysis/src/TypeChecker2.cpp @@ -1561,6 +1561,18 @@ struct TypeChecker2 else reportError(CannotExtendTable{exprType, CannotExtendTable::Indexer, "indexer??"}, indexExpr->location); } + else if (auto mt = get(exprType)) + { + const TableType* tt = get(follow(mt->table)); + LUAU_ASSERT(tt); + if (tt->indexer) + testIsSubtype(indexType, tt->indexer->indexType, indexExpr->index->location); + else + { + // TODO: Maybe the metatable has a suitable indexer? + reportError(CannotExtendTable{exprType, CannotExtendTable::Indexer, "indexer??"}, indexExpr->location); + } + } else if (auto cls = get(exprType)) { if (cls->indexer) @@ -1581,6 +1593,19 @@ struct TypeChecker2 reportError(OptionalValueAccess{exprType}, indexExpr->location); } } + else if (auto exprIntersection = get(exprType)) + { + for (TypeId part : exprIntersection) + { + (void)part; + } + } + else if (get(exprType) || isErrorSuppressing(indexExpr->location, exprType)) + { + // Nothing + } + else + reportError(NotATable{exprType}, indexExpr->location); } void visit(AstExprFunction* fn) @@ -2720,6 +2745,8 @@ struct TypeChecker2 fetch(builtinTypes->stringType); if (normValid) fetch(norm->threads); + if (normValid) + fetch(norm->buffers); if (normValid) { diff --git a/Analysis/src/TypeFamily.cpp b/Analysis/src/TypeFamily.cpp index a8d7d2f7..3a0483a6 100644 --- a/Analysis/src/TypeFamily.cpp +++ b/Analysis/src/TypeFamily.cpp @@ -11,12 +11,10 @@ #include "Luau/OverloadResolution.h" #include "Luau/Set.h" #include "Luau/Simplify.h" -#include "Luau/Substitution.h" #include "Luau/Subtyping.h" #include "Luau/ToString.h" #include "Luau/TxnLog.h" #include "Luau/Type.h" -#include "Luau/TypeCheckLimits.h" #include "Luau/TypeFamilyReductionGuesser.h" #include "Luau/TypeFwd.h" #include "Luau/TypeUtils.h" @@ -346,9 +344,8 @@ struct FamilyReducer if (tryGuessing(subject)) return; - TypeFamilyQueue queue{NotNull{&queuedTys}, NotNull{&queuedTps}}; TypeFamilyReductionResult result = - tfit->family->reducer(subject, NotNull{&queue}, tfit->typeArguments, tfit->packArguments, NotNull{&ctx}); + tfit->family->reducer(subject, tfit->typeArguments, tfit->packArguments, NotNull{&ctx}); handleFamilyReduction(subject, result); } } @@ -372,9 +369,8 @@ struct FamilyReducer if (tryGuessing(subject)) return; - TypeFamilyQueue queue{NotNull{&queuedTys}, NotNull{&queuedTps}}; TypeFamilyReductionResult result = - tfit->family->reducer(subject, NotNull{&queue}, tfit->typeArguments, tfit->packArguments, NotNull{&ctx}); + tfit->family->reducer(subject, tfit->typeArguments, tfit->packArguments, NotNull{&ctx}); handleFamilyReduction(subject, result); } } @@ -449,24 +445,89 @@ FamilyGraphReductionResult reduceFamilies(TypePackId entrypoint, Location locati std::move(collector.cyclicInstance), location, ctx, force); } -void TypeFamilyQueue::add(TypeId instanceTy) -{ - LUAU_ASSERT(get(instanceTy)); - queuedTys->push_back(instanceTy); -} - -void TypeFamilyQueue::add(TypePackId instanceTp) -{ - LUAU_ASSERT(get(instanceTp)); - queuedTps->push_back(instanceTp); -} - bool isPending(TypeId ty, ConstraintSolver* solver) { return is(ty) || (solver && solver->hasUnresolvedConstraints(ty)); } -TypeFamilyReductionResult notFamilyFn(TypeId instance, NotNull queue, const std::vector& typeParams, +template +static std::optional> tryDistributeTypeFamilyApp(F f, TypeId instance, + const std::vector& typeParams, const std::vector& packParams, NotNull ctx, Args&& ...args) +{ + // op (a | b) (c | d) ~ (op a (c | d)) | (op b (c | d)) ~ (op a c) | (op a d) | (op b c) | (op b d) + bool uninhabited = false; + std::vector blockedTypes; + std::vector results; + size_t cartesianProductSize = 1; + + const UnionType* firstUnion = nullptr; + size_t unionIndex; + + std::vector arguments = typeParams; + for (size_t i = 0; i < arguments.size(); ++i) + { + const UnionType* ut = get(follow(arguments[i])); + if (!ut) + continue; + + // We want to find the first union type in the set of arguments to distribute that one and only that one union. + // The function `f` we have is recursive, so `arguments[unionIndex]` will be updated in-place for each option in + // the union we've found in this context, so that index will no longer be a union type. Any other arguments at + // index + 1 or after will instead be distributed, if those are a union, which will be subjected to the same rules. + if (!firstUnion && ut) + { + firstUnion = ut; + unionIndex = i; + } + + cartesianProductSize *= std::distance(begin(ut), end(ut)); + + // TODO: We'd like to report that the type family application is too complex here. + if (size_t(DFInt::LuauTypeFamilyApplicationCartesianProductLimit) <= cartesianProductSize) + return {{std::nullopt, true, {}, {}}}; + } + + if (!firstUnion) + { + // If we couldn't find any union type argument, we're not distributing. + return std::nullopt; + } + + for (TypeId option : firstUnion) + { + arguments[unionIndex] = option; + + TypeFamilyReductionResult result = f(instance, arguments, packParams, ctx, args...); + blockedTypes.insert(blockedTypes.end(), result.blockedTypes.begin(), result.blockedTypes.end()); + uninhabited |= result.uninhabited; + + if (result.uninhabited || !result.result) + break; + else + results.push_back(*result.result); + } + + if (uninhabited || !blockedTypes.empty()) + return {{std::nullopt, uninhabited, blockedTypes, {}}}; + + if (!results.empty()) + { + if (results.size() == 1) + return {{results[0], false, {}, {}}}; + + TypeId resultTy = ctx->arena->addType(TypeFamilyInstanceType{ + NotNull{&kBuiltinTypeFamilies.unionFamily}, + std::move(results), + {}, + }); + + return {{resultTy, false, {}, {}}}; + } + + return std::nullopt; +} + +TypeFamilyReductionResult notFamilyFn(TypeId instance, const std::vector& typeParams, const std::vector& packParams, NotNull ctx) { if (typeParams.size() != 1 || !packParams.empty()) @@ -477,14 +538,20 @@ TypeFamilyReductionResult notFamilyFn(TypeId instance, NotNullbuiltins->neverType, false, {}, {}}; + if (isPending(ty, ctx->solver)) return {std::nullopt, false, {ty}, {}}; + if (auto result = tryDistributeTypeFamilyApp(notFamilyFn, instance, typeParams, packParams, ctx)) + return *result; + // `not` operates on anything and returns a `boolean` always. return {ctx->builtins->booleanType, false, {}, {}}; } -TypeFamilyReductionResult lenFamilyFn(TypeId instance, NotNull queue, const std::vector& typeParams, +TypeFamilyReductionResult lenFamilyFn(TypeId instance, const std::vector& typeParams, const std::vector& packParams, NotNull ctx) { if (typeParams.size() != 1 || !packParams.empty()) @@ -495,6 +562,9 @@ TypeFamilyReductionResult lenFamilyFn(TypeId instance, NotNullbuiltins->neverType, false, {}, {}}; + // check to see if the operand type is resolved enough, and wait to reduce if not // the use of `typeFromNormal` later necessitates blocking on local types. if (isPending(operandTy, ctx->solver) || get(operandTy)) @@ -533,6 +603,9 @@ TypeFamilyReductionResult lenFamilyFn(TypeId instance, NotNullhasTopTable() || get(normalizedOperand)) return {ctx->builtins->numberType, false, {}, {}}; + if (auto result = tryDistributeTypeFamilyApp(notFamilyFn, instance, typeParams, packParams, ctx)) + return *result; + // findMetatableEntry demands the ability to emit errors, so we must give it // the necessary state to do that, even if we intend to just eat the errors. ErrorVec dummy; @@ -570,7 +643,7 @@ TypeFamilyReductionResult lenFamilyFn(TypeId instance, NotNullbuiltins->numberType, false, {}, {}}; } -TypeFamilyReductionResult unmFamilyFn(TypeId instance, NotNull queue, const std::vector& typeParams, +TypeFamilyReductionResult unmFamilyFn(TypeId instance, const std::vector& typeParams, const std::vector& packParams, NotNull ctx) { if (typeParams.size() != 1 || !packParams.empty()) @@ -581,6 +654,9 @@ TypeFamilyReductionResult unmFamilyFn(TypeId instance, NotNullbuiltins->neverType, false, {}, {}}; + // check to see if the operand type is resolved enough, and wait to reduce if not if (isPending(operandTy, ctx->solver)) return {std::nullopt, false, {operandTy}, {}}; @@ -612,6 +688,9 @@ TypeFamilyReductionResult unmFamilyFn(TypeId instance, NotNullisExactlyNumber()) return {ctx->builtins->numberType, false, {}, {}}; + if (auto result = tryDistributeTypeFamilyApp(notFamilyFn, instance, typeParams, packParams, ctx)) + return *result; + // findMetatableEntry demands the ability to emit errors, so we must give it // the necessary state to do that, even if we intend to just eat the errors. ErrorVec dummy; @@ -664,7 +743,7 @@ NotNull TypeFamilyContext::pushConstraint(ConstraintV&& c) return newConstraint; } -TypeFamilyReductionResult numericBinopFamilyFn(TypeId instance, NotNull queue, const std::vector& typeParams, +TypeFamilyReductionResult numericBinopFamilyFn(TypeId instance, const std::vector& typeParams, const std::vector& packParams, NotNull ctx, const std::string metamethod) { if (typeParams.size() != 2 || !packParams.empty()) @@ -723,67 +802,8 @@ TypeFamilyReductionResult numericBinopFamilyFn(TypeId instance, NotNull< if (normLhsTy->isExactlyNumber() && normRhsTy->isExactlyNumber()) return {ctx->builtins->numberType, false, {}, {}}; - // op (a | b) (c | d) ~ (op a (c | d)) | (op b (c | d)) ~ (op a c) | (op a d) | (op b c) | (op b d) - std::vector results; - bool uninhabited = false; - std::vector blockedTypes; - std::vector arguments = typeParams; - auto distributeFamilyApp = [&](const UnionType* ut, size_t argumentIndex) { - // Returning true here means we completed the loop without any problems. - for (TypeId option : ut) - { - arguments[argumentIndex] = option; - - TypeFamilyReductionResult result = numericBinopFamilyFn(instance, queue, arguments, packParams, ctx, metamethod); - blockedTypes.insert(blockedTypes.end(), result.blockedTypes.begin(), result.blockedTypes.end()); - uninhabited |= result.uninhabited; - - if (result.uninhabited) - return false; - else if (!result.result) - return false; - else - results.push_back(*result.result); - } - - return true; - }; - - const UnionType* lhsUnion = get(lhsTy); - const UnionType* rhsUnion = get(rhsTy); - if (lhsUnion || rhsUnion) - { - // TODO: We'd like to report that the type family application is too complex here. - size_t lhsUnionSize = lhsUnion ? std::distance(begin(lhsUnion), end(lhsUnion)) : 1; - size_t rhsUnionSize = rhsUnion ? std::distance(begin(rhsUnion), end(rhsUnion)) : 1; - if (size_t(DFInt::LuauTypeFamilyApplicationCartesianProductLimit) <= lhsUnionSize * rhsUnionSize) - return {std::nullopt, true, {}, {}}; - - if (lhsUnion && !distributeFamilyApp(lhsUnion, 0)) - return {std::nullopt, uninhabited, std::move(blockedTypes), {}}; - - if (rhsUnion && !distributeFamilyApp(rhsUnion, 1)) - return {std::nullopt, uninhabited, std::move(blockedTypes), {}}; - - if (results.empty()) - { - // If this happens, it means `distributeFamilyApp` has improperly returned `true` even - // though there exists no arm of the union that is inhabited or have a reduced type. - ctx->ice->ice("`distributeFamilyApp` failed to add any types to the results vector?"); - } - - if (results.size() == 1) - return {results[0], false, {}, {}}; - - TypeId resultTy = ctx->arena->addType(TypeFamilyInstanceType{ - NotNull{&kBuiltinTypeFamilies.unionFamily}, - std::move(results), - {}, - }); - - queue->add(resultTy); - return {resultTy, false, {}, {}}; - } + if (auto result = tryDistributeTypeFamilyApp(numericBinopFamilyFn, instance, typeParams, packParams, ctx, metamethod)) + return *result; // findMetatableEntry demands the ability to emit errors, so we must give it // the necessary state to do that, even if we intend to just eat the errors. @@ -826,7 +846,7 @@ TypeFamilyReductionResult numericBinopFamilyFn(TypeId instance, NotNull< return {extracted.head.front(), false, {}, {}}; } -TypeFamilyReductionResult addFamilyFn(TypeId instance, NotNull queue, const std::vector& typeParams, +TypeFamilyReductionResult addFamilyFn(TypeId instance, const std::vector& typeParams, const std::vector& packParams, NotNull ctx) { if (typeParams.size() != 2 || !packParams.empty()) @@ -835,10 +855,10 @@ TypeFamilyReductionResult addFamilyFn(TypeId instance, NotNull subFamilyFn(TypeId instance, NotNull queue, const std::vector& typeParams, +TypeFamilyReductionResult subFamilyFn(TypeId instance, const std::vector& typeParams, const std::vector& packParams, NotNull ctx) { if (typeParams.size() != 2 || !packParams.empty()) @@ -847,10 +867,10 @@ TypeFamilyReductionResult subFamilyFn(TypeId instance, NotNull mulFamilyFn(TypeId instance, NotNull queue, const std::vector& typeParams, +TypeFamilyReductionResult mulFamilyFn(TypeId instance, const std::vector& typeParams, const std::vector& packParams, NotNull ctx) { if (typeParams.size() != 2 || !packParams.empty()) @@ -859,10 +879,10 @@ TypeFamilyReductionResult mulFamilyFn(TypeId instance, NotNull divFamilyFn(TypeId instance, NotNull queue, const std::vector& typeParams, +TypeFamilyReductionResult divFamilyFn(TypeId instance, const std::vector& typeParams, const std::vector& packParams, NotNull ctx) { if (typeParams.size() != 2 || !packParams.empty()) @@ -871,10 +891,10 @@ TypeFamilyReductionResult divFamilyFn(TypeId instance, NotNull idivFamilyFn(TypeId instance, NotNull queue, const std::vector& typeParams, +TypeFamilyReductionResult idivFamilyFn(TypeId instance, const std::vector& typeParams, const std::vector& packParams, NotNull ctx) { if (typeParams.size() != 2 || !packParams.empty()) @@ -883,10 +903,10 @@ TypeFamilyReductionResult idivFamilyFn(TypeId instance, NotNull powFamilyFn(TypeId instance, NotNull queue, const std::vector& typeParams, +TypeFamilyReductionResult powFamilyFn(TypeId instance, const std::vector& typeParams, const std::vector& packParams, NotNull ctx) { if (typeParams.size() != 2 || !packParams.empty()) @@ -895,10 +915,10 @@ TypeFamilyReductionResult powFamilyFn(TypeId instance, NotNull modFamilyFn(TypeId instance, NotNull queue, const std::vector& typeParams, +TypeFamilyReductionResult modFamilyFn(TypeId instance, const std::vector& typeParams, const std::vector& packParams, NotNull ctx) { if (typeParams.size() != 2 || !packParams.empty()) @@ -907,10 +927,10 @@ TypeFamilyReductionResult modFamilyFn(TypeId instance, NotNull concatFamilyFn(TypeId instance, NotNull queue, const std::vector& typeParams, +TypeFamilyReductionResult concatFamilyFn(TypeId instance, const std::vector& typeParams, const std::vector& packParams, NotNull ctx) { if (typeParams.size() != 2 || !packParams.empty()) @@ -922,6 +942,10 @@ TypeFamilyReductionResult concatFamilyFn(TypeId instance, NotNullbuiltins->neverType, false, {}, {}}; + // check to see if both operand types are resolved enough, and wait to reduce if not if (isPending(lhsTy, ctx->solver)) return {std::nullopt, false, {lhsTy}, {}}; @@ -962,6 +986,9 @@ TypeFamilyReductionResult concatFamilyFn(TypeId instance, NotNullisSubtypeOfString() || normLhsTy->isExactlyNumber()) && (normRhsTy->isSubtypeOfString() || normRhsTy->isExactlyNumber())) return {ctx->builtins->stringType, false, {}, {}}; + if (auto result = tryDistributeTypeFamilyApp(concatFamilyFn, instance, typeParams, packParams, ctx)) + return *result; + // findMetatableEntry demands the ability to emit errors, so we must give it // the necessary state to do that, even if we intend to just eat the errors. ErrorVec dummy; @@ -1011,7 +1038,7 @@ TypeFamilyReductionResult concatFamilyFn(TypeId instance, NotNullbuiltins->stringType, false, {}, {}}; } -TypeFamilyReductionResult andFamilyFn(TypeId instance, NotNull queue, const std::vector& typeParams, +TypeFamilyReductionResult andFamilyFn(TypeId instance, const std::vector& typeParams, const std::vector& packParams, NotNull ctx) { if (typeParams.size() != 2 || !packParams.empty()) @@ -1062,7 +1089,7 @@ TypeFamilyReductionResult andFamilyFn(TypeId instance, NotNull orFamilyFn(TypeId instance, NotNull queue, const std::vector& typeParams, +TypeFamilyReductionResult orFamilyFn(TypeId instance, const std::vector& typeParams, const std::vector& packParams, NotNull ctx) { if (typeParams.size() != 2 || !packParams.empty()) @@ -1113,7 +1140,7 @@ TypeFamilyReductionResult orFamilyFn(TypeId instance, NotNull comparisonFamilyFn(TypeId instance, NotNull queue, const std::vector& typeParams, +static TypeFamilyReductionResult comparisonFamilyFn(TypeId instance, const std::vector& typeParams, const std::vector& packParams, NotNull ctx, const std::string metamethod) { @@ -1126,6 +1153,9 @@ static TypeFamilyReductionResult comparisonFamilyFn(TypeId instance, Not TypeId lhsTy = follow(typeParams.at(0)); TypeId rhsTy = follow(typeParams.at(1)); + if (lhsTy == instance || rhsTy == instance) + return {ctx->builtins->neverType, false, {}, {}}; + if (isPending(lhsTy, ctx->solver)) return {std::nullopt, false, {lhsTy}, {}}; else if (isPending(rhsTy, ctx->solver)) @@ -1207,6 +1237,9 @@ static TypeFamilyReductionResult comparisonFamilyFn(TypeId instance, Not if (normLhsTy->isExactlyNumber() && normRhsTy->isExactlyNumber()) return {ctx->builtins->booleanType, false, {}, {}}; + if (auto result = tryDistributeTypeFamilyApp(comparisonFamilyFn, instance, typeParams, packParams, ctx, metamethod)) + return *result; + // findMetatableEntry demands the ability to emit errors, so we must give it // the necessary state to do that, even if we intend to just eat the errors. ErrorVec dummy; @@ -1246,7 +1279,7 @@ static TypeFamilyReductionResult comparisonFamilyFn(TypeId instance, Not return {ctx->builtins->booleanType, false, {}, {}}; } -TypeFamilyReductionResult ltFamilyFn(TypeId instance, NotNull queue, const std::vector& typeParams, +TypeFamilyReductionResult ltFamilyFn(TypeId instance, const std::vector& typeParams, const std::vector& packParams, NotNull ctx) { if (typeParams.size() != 2 || !packParams.empty()) @@ -1255,10 +1288,10 @@ TypeFamilyReductionResult ltFamilyFn(TypeId instance, NotNull leFamilyFn(TypeId instance, NotNull queue, const std::vector& typeParams, +TypeFamilyReductionResult leFamilyFn(TypeId instance, const std::vector& typeParams, const std::vector& packParams, NotNull ctx) { if (typeParams.size() != 2 || !packParams.empty()) @@ -1267,10 +1300,10 @@ TypeFamilyReductionResult leFamilyFn(TypeId instance, NotNull eqFamilyFn(TypeId instance, NotNull queue, const std::vector& typeParams, +TypeFamilyReductionResult eqFamilyFn(TypeId instance, const std::vector& typeParams, const std::vector& packParams, NotNull ctx) { if (typeParams.size() != 2 || !packParams.empty()) @@ -1407,7 +1440,7 @@ struct FindRefinementBlockers : TypeOnceVisitor }; -TypeFamilyReductionResult refineFamilyFn(TypeId instance, NotNull queue, const std::vector& typeParams, +TypeFamilyReductionResult refineFamilyFn(TypeId instance, const std::vector& typeParams, const std::vector& packParams, NotNull ctx) { if (typeParams.size() != 2 || !packParams.empty()) @@ -1480,7 +1513,7 @@ TypeFamilyReductionResult refineFamilyFn(TypeId instance, NotNull singletonFamilyFn(TypeId instance, NotNull queue, const std::vector& typeParams, +TypeFamilyReductionResult singletonFamilyFn(TypeId instance, const std::vector& typeParams, const std::vector& packParams, NotNull ctx) { if (typeParams.size() != 1 || !packParams.empty()) @@ -1517,7 +1550,7 @@ TypeFamilyReductionResult singletonFamilyFn(TypeId instance, NotNullbuiltins->unknownType, false, {}, {}}; } -TypeFamilyReductionResult unionFamilyFn(TypeId instance, NotNull queue, const std::vector& typeParams, +TypeFamilyReductionResult unionFamilyFn(TypeId instance, const std::vector& typeParams, const std::vector& packParams, NotNull ctx) { if (!packParams.empty()) @@ -1578,7 +1611,7 @@ TypeFamilyReductionResult unionFamilyFn(TypeId instance, NotNull intersectFamilyFn(TypeId instance, NotNull queue, const std::vector& typeParams, +TypeFamilyReductionResult intersectFamilyFn(TypeId instance, const std::vector& typeParams, const std::vector& packParams, NotNull ctx) { if (!packParams.empty()) @@ -1802,7 +1835,7 @@ TypeFamilyReductionResult keyofFamilyImpl( return {ctx->arena->addType(UnionType{singletons}), false, {}, {}}; } -TypeFamilyReductionResult keyofFamilyFn(TypeId instance, NotNull queue, const std::vector& typeParams, +TypeFamilyReductionResult keyofFamilyFn(TypeId instance, const std::vector& typeParams, const std::vector& packParams, NotNull ctx) { if (typeParams.size() != 1 || !packParams.empty()) @@ -1814,7 +1847,7 @@ TypeFamilyReductionResult keyofFamilyFn(TypeId instance, NotNull rawkeyofFamilyFn(TypeId instance, NotNull queue, const std::vector& typeParams, +TypeFamilyReductionResult rawkeyofFamilyFn(TypeId instance, const std::vector& typeParams, const std::vector& packParams, NotNull ctx) { if (typeParams.size() != 1 || !packParams.empty()) diff --git a/Analysis/src/TypeUtils.cpp b/Analysis/src/TypeUtils.cpp index 588b1da1..c2512ddc 100644 --- a/Analysis/src/TypeUtils.cpp +++ b/Analysis/src/TypeUtils.cpp @@ -38,6 +38,59 @@ bool occursCheck(TypeId needle, TypeId haystack) return false; } +// FIXME: Property is quite large. +// +// Returning it on the stack like this isn't great. We'd like to just return a +// const Property*, but we mint a property of type any if the subject type is +// any. +std::optional findTableProperty(NotNull builtinTypes, ErrorVec& errors, TypeId ty, const std::string& name, Location location) +{ + if (get(ty)) + return Property::rw(ty); + + if (const TableType* tableType = getTableType(ty)) + { + const auto& it = tableType->props.find(name); + if (it != tableType->props.end()) + return it->second; + } + + std::optional mtIndex = findMetatableEntry(builtinTypes, errors, ty, "__index", location); + int count = 0; + while (mtIndex) + { + TypeId index = follow(*mtIndex); + + if (count >= 100) + return std::nullopt; + + ++count; + + if (const auto& itt = getTableType(index)) + { + const auto& fit = itt->props.find(name); + if (fit != itt->props.end()) + return fit->second.type(); + } + else if (const auto& itf = get(index)) + { + std::optional r = first(follow(itf->retTypes)); + if (!r) + return builtinTypes->nilType; + else + return *r; + } + else if (get(index)) + return builtinTypes->anyType; + else + errors.push_back(TypeError{location, GenericError{"__index should either be a function or table. Got " + toString(index)}}); + + mtIndex = findMetatableEntry(builtinTypes, errors, *mtIndex, "__index", location); + } + + return std::nullopt; +} + std::optional findMetatableEntry( NotNull builtinTypes, ErrorVec& errors, TypeId type, const std::string& entry, Location location) { diff --git a/CLI/Repl.cpp b/CLI/Repl.cpp index 501707e1..814d7c8c 100644 --- a/CLI/Repl.cpp +++ b/CLI/Repl.cpp @@ -256,12 +256,16 @@ void setupState(lua_State* L) void setupArguments(lua_State* L, int argc, char** argv) { + lua_checkstack(L, argc); + for (int i = 0; i < argc; ++i) lua_pushstring(L, argv[i]); } std::string runCode(lua_State* L, const std::string& source) { + lua_checkstack(L, LUA_MINSTACK); + std::string bytecode = Luau::compile(source, copts()); if (luau_load(L, "=stdin", bytecode.data(), bytecode.size(), 0) != 0) @@ -432,6 +436,8 @@ static void completeIndexer(lua_State* L, const std::string& editBuffer, const A std::string_view lookup = editBuffer; bool completeOnlyFunctions = false; + lua_checkstack(L, LUA_MINSTACK); + // Push the global variable table to begin the search lua_pushvalue(L, LUA_GLOBALSINDEX); diff --git a/CodeGen/include/Luau/CodeGen.h b/CodeGen/include/Luau/CodeGen.h index ac444b7b..171e9197 100644 --- a/CodeGen/include/Luau/CodeGen.h +++ b/CodeGen/include/Luau/CodeGen.h @@ -92,7 +92,7 @@ struct HostIrHooks // Guards should take a VM exit to 'pcpos' HostVectorAccessHandler vectorAccess = nullptr; - // Handle namecalled performed on a vector value + // Handle namecall performed on a vector value // 'sourceReg' (self argument) is guaranteed to be a vector // All other arguments can be of any type // Guards should take a VM exit to 'pcpos' @@ -103,6 +103,9 @@ struct CompilationOptions { unsigned int flags = 0; HostIrHooks hooks; + + // null-terminated array of userdata types names that might have custom lowering + const char* const* userdataTypes = nullptr; }; struct CompilationStats @@ -163,6 +166,12 @@ void create(lua_State* L, SharedCodeGenContext* codeGenContext); // Enable or disable native execution according to `enabled` argument void setNativeExecutionEnabled(lua_State* L, bool enabled); +// Given a name, this function must return the index of the type which matches the type array used all CompilationOptions and AssemblyOptions +// If the type is unknown, 0xff has to be returned +using UserdataRemapperCallback = uint8_t(void* context, const char* name, size_t nameLength); + +void setUserdataRemapper(lua_State* L, void* context, UserdataRemapperCallback cb); + using ModuleId = std::array; // Builds target function and all inner functions diff --git a/CodeGen/include/Luau/IrDump.h b/CodeGen/include/Luau/IrDump.h index dcca3c7b..d989a6c7 100644 --- a/CodeGen/include/Luau/IrDump.h +++ b/CodeGen/include/Luau/IrDump.h @@ -31,9 +31,11 @@ void toString(IrToStringContext& ctx, IrOp op); void toString(std::string& result, IrConst constant); -const char* getBytecodeTypeName(uint8_t type); +const char* getBytecodeTypeName_DEPRECATED(uint8_t type); +const char* getBytecodeTypeName(uint8_t type, const char* const* userdataTypes); -void toString(std::string& result, const BytecodeTypes& bcTypes); +void toString_DEPRECATED(std::string& result, const BytecodeTypes& bcTypes); +void toString(std::string& result, const BytecodeTypes& bcTypes, const char* const* userdataTypes); void toStringDetailed( IrToStringContext& ctx, const IrBlock& block, uint32_t blockIdx, const IrInst& inst, uint32_t instIdx, IncludeUseInfo includeUseInfo); diff --git a/CodeGen/include/Luau/IrUtils.h b/CodeGen/include/Luau/IrUtils.h index 0c8495e8..55b86822 100644 --- a/CodeGen/include/Luau/IrUtils.h +++ b/CodeGen/include/Luau/IrUtils.h @@ -241,6 +241,10 @@ IrValueKind getCmdValueKind(IrCmd cmd); bool isGCO(uint8_t tag); +// Optional bit has to be cleared at call site, otherwise, this will return 'false' for 'userdata?' +bool isUserdataBytecodeType(uint8_t ty); +bool isCustomUserdataBytecodeType(uint8_t ty); + // Manually add or remove use of an operand void addUse(IrFunction& function, IrOp op); void removeUse(IrFunction& function, IrOp op); diff --git a/CodeGen/src/BytecodeAnalysis.cpp b/CodeGen/src/BytecodeAnalysis.cpp index 900093d1..aed8c763 100644 --- a/CodeGen/src/BytecodeAnalysis.cpp +++ b/CodeGen/src/BytecodeAnalysis.cpp @@ -14,8 +14,6 @@ LUAU_FASTFLAG(LuauCodegenDirectUserdataFlow) LUAU_FASTFLAG(LuauLoadTypeInfo) // Because new VM typeinfo load changes the format used by Codegen, same flag is used LUAU_FASTFLAGVARIABLE(LuauCodegenTypeInfo, false) // New analysis is flagged separately -LUAU_FASTFLAG(LuauTypeInfoLookupImprovement) -LUAU_FASTFLAGVARIABLE(LuauCodegenVectorMispredictFix, false) LUAU_FASTFLAGVARIABLE(LuauCodegenAnalyzeHostVectorOps, false) LUAU_FASTFLAGVARIABLE(LuauCodegenLoadTypeUpvalCheck, false) @@ -68,21 +66,13 @@ void loadBytecodeTypeInfo(IrFunction& function) Proto* proto = function.proto; - if (FFlag::LuauTypeInfoLookupImprovement) - { - if (!proto) - return; - } - else - { - if (!proto || !proto->typeinfo) - return; - } + if (!proto) + return; BytecodeTypeInfo& typeInfo = function.bcTypeInfo; // If there is no typeinfo, we generate default values for arguments and upvalues - if (FFlag::LuauTypeInfoLookupImprovement && !proto->typeinfo) + if (!proto->typeinfo) { typeInfo.argumentTypes.resize(proto->numparams, LBC_TYPE_ANY); typeInfo.upvalueTypes.resize(proto->nups, LBC_TYPE_ANY); @@ -150,8 +140,6 @@ void loadBytecodeTypeInfo(IrFunction& function) static void prepareRegTypeInfoLookups(BytecodeTypeInfo& typeInfo) { - CODEGEN_ASSERT(FFlag::LuauTypeInfoLookupImprovement); - // Sort by register first, then by end PC std::sort(typeInfo.regTypes.begin(), typeInfo.regTypes.end(), [](const BytecodeRegTypeInfo& a, const BytecodeRegTypeInfo& b) { if (a.reg != b.reg) @@ -186,39 +174,26 @@ static BytecodeRegTypeInfo* findRegType(BytecodeTypeInfo& info, uint8_t reg, int { CODEGEN_ASSERT(FFlag::LuauCodegenTypeInfo); - if (FFlag::LuauTypeInfoLookupImprovement) - { - auto b = info.regTypes.begin() + info.regTypeOffsets[reg]; - auto e = info.regTypes.begin() + info.regTypeOffsets[reg + 1]; - - // Doen't have info - if (b == e) - return nullptr; - - // No info after the last live range - if (pc >= (e - 1)->endpc) - return nullptr; - - for (auto it = b; it != e; ++it) - { - CODEGEN_ASSERT(it->reg == reg); - - if (pc >= it->startpc && pc < it->endpc) - return &*it; - } + auto b = info.regTypes.begin() + info.regTypeOffsets[reg]; + auto e = info.regTypes.begin() + info.regTypeOffsets[reg + 1]; + // Doen't have info + if (b == e) return nullptr; - } - else - { - for (BytecodeRegTypeInfo& el : info.regTypes) - { - if (reg == el.reg && pc >= el.startpc && pc < el.endpc) - return ⪙ - } + // No info after the last live range + if (pc >= (e - 1)->endpc) return nullptr; + + for (auto it = b; it != e; ++it) + { + CODEGEN_ASSERT(it->reg == reg); + + if (pc >= it->startpc && pc < it->endpc) + return &*it; } + + return nullptr; } static void refineRegType(BytecodeTypeInfo& info, uint8_t reg, int pc, uint8_t ty) @@ -233,7 +208,7 @@ static void refineRegType(BytecodeTypeInfo& info, uint8_t reg, int pc, uint8_t t if (regType->type == LBC_TYPE_ANY) regType->type = ty; } - else if (FFlag::LuauTypeInfoLookupImprovement && reg < info.argumentTypes.size()) + else if (reg < info.argumentTypes.size()) { if (info.argumentTypes[reg] == LBC_TYPE_ANY) info.argumentTypes[reg] = ty; @@ -627,8 +602,7 @@ void analyzeBytecodeTypes(IrFunction& function, const HostIrHooks& hostHooks) BytecodeTypeInfo& bcTypeInfo = function.bcTypeInfo; - if (FFlag::LuauTypeInfoLookupImprovement) - prepareRegTypeInfoLookups(bcTypeInfo); + prepareRegTypeInfoLookups(bcTypeInfo); // Setup our current knowledge of type tags based on arguments uint8_t regTags[256]; @@ -786,32 +760,22 @@ void analyzeBytecodeTypes(IrFunction& function, const HostIrHooks& hostHooks) regTags[ra] = LBC_TYPE_ANY; - if (FFlag::LuauCodegenVectorMispredictFix) + if (bcType.a == LBC_TYPE_VECTOR) { - if (bcType.a == LBC_TYPE_VECTOR) + TString* str = gco2ts(function.proto->k[kc].value.gc); + const char* field = getstr(str); + + if (str->len == 1) { - TString* str = gco2ts(function.proto->k[kc].value.gc); - const char* field = getstr(str); + // Same handling as LOP_GETTABLEKS block in lvmexecute.cpp - case-insensitive comparison with "X" / "Y" / "Z" + char ch = field[0] | ' '; - if (str->len == 1) - { - // Same handling as LOP_GETTABLEKS block in lvmexecute.cpp - case-insensitive comparison with "X" / "Y" / "Z" - char ch = field[0] | ' '; - - if (ch == 'x' || ch == 'y' || ch == 'z') - regTags[ra] = LBC_TYPE_NUMBER; - } - - if (FFlag::LuauCodegenAnalyzeHostVectorOps && regTags[ra] == LBC_TYPE_ANY && hostHooks.vectorAccessBytecodeType) - regTags[ra] = hostHooks.vectorAccessBytecodeType(field, str->len); + if (ch == 'x' || ch == 'y' || ch == 'z') + regTags[ra] = LBC_TYPE_NUMBER; } - } - else - { - // Assuming that vector component is being indexed - // TODO: check what key is used - if (bcType.a == LBC_TYPE_VECTOR) - regTags[ra] = LBC_TYPE_NUMBER; + + if (FFlag::LuauCodegenAnalyzeHostVectorOps && regTags[ra] == LBC_TYPE_ANY && hostHooks.vectorAccessBytecodeType) + regTags[ra] = hostHooks.vectorAccessBytecodeType(field, str->len); } bcType.result = regTags[ra]; diff --git a/CodeGen/src/CodeGenAssembly.cpp b/CodeGen/src/CodeGenAssembly.cpp index ce3a57bd..269bf8dc 100644 --- a/CodeGen/src/CodeGenAssembly.cpp +++ b/CodeGen/src/CodeGenAssembly.cpp @@ -13,7 +13,7 @@ #include "lapi.h" LUAU_FASTFLAG(LuauCodegenTypeInfo) -LUAU_FASTFLAGVARIABLE(LuauCodegenIrTypeNames, false) +LUAU_FASTFLAG(LuauLoadUserdataInfo) namespace Luau { @@ -22,8 +22,6 @@ namespace CodeGen static const LocVar* tryFindLocal(const Proto* proto, int reg, int pcpos) { - CODEGEN_ASSERT(FFlag::LuauCodegenIrTypeNames); - for (int i = 0; i < proto->sizelocvars; i++) { const LocVar& local = proto->locvars[i]; @@ -37,8 +35,6 @@ static const LocVar* tryFindLocal(const Proto* proto, int reg, int pcpos) const char* tryFindLocalName(const Proto* proto, int reg, int pcpos) { - CODEGEN_ASSERT(FFlag::LuauCodegenIrTypeNames); - const LocVar* var = tryFindLocal(proto, reg, pcpos); if (var && var->varname) @@ -49,8 +45,6 @@ const char* tryFindLocalName(const Proto* proto, int reg, int pcpos) const char* tryFindUpvalueName(const Proto* proto, int upval) { - CODEGEN_ASSERT(FFlag::LuauCodegenIrTypeNames); - if (proto->upvalues) { CODEGEN_ASSERT(upval < proto->sizeupvalues); @@ -72,22 +66,10 @@ static void logFunctionHeader(AssemblyBuilder& build, Proto* proto) for (int i = 0; i < proto->numparams; i++) { - if (FFlag::LuauCodegenIrTypeNames) - { - if (const char* name = tryFindLocalName(proto, i, 0)) - build.logAppend("%s%s", i == 0 ? "" : ", ", name); - else - build.logAppend("%s$arg%d", i == 0 ? "" : ", ", i); - } + if (const char* name = tryFindLocalName(proto, i, 0)) + build.logAppend("%s%s", i == 0 ? "" : ", ", name); else - { - LocVar* var = proto->locvars ? &proto->locvars[proto->sizelocvars - proto->numparams + i] : nullptr; - - if (var && var->varname) - build.logAppend("%s%s", i == 0 ? "" : ", ", getstr(var->varname)); - else - build.logAppend("%s$arg%d", i == 0 ? "" : ", ", i); - } + build.logAppend("%s$arg%d", i == 0 ? "" : ", ", i); } if (proto->numparams != 0 && proto->is_vararg) @@ -102,9 +84,10 @@ static void logFunctionHeader(AssemblyBuilder& build, Proto* proto) } template -static void logFunctionTypes(AssemblyBuilder& build, const IrFunction& function) +static void logFunctionTypes_DEPRECATED(AssemblyBuilder& build, const IrFunction& function) { CODEGEN_ASSERT(FFlag::LuauCodegenTypeInfo); + CODEGEN_ASSERT(!FFlag::LuauLoadUserdataInfo); const BytecodeTypeInfo& typeInfo = function.bcTypeInfo; @@ -112,20 +95,12 @@ static void logFunctionTypes(AssemblyBuilder& build, const IrFunction& function) { uint8_t ty = typeInfo.argumentTypes[i]; - if (FFlag::LuauCodegenIrTypeNames) + if (ty != LBC_TYPE_ANY) { - if (ty != LBC_TYPE_ANY) - { - if (const char* name = tryFindLocalName(function.proto, int(i), 0)) - build.logAppend("; R%d: %s [argument '%s']\n", int(i), getBytecodeTypeName(ty), name); - else - build.logAppend("; R%d: %s [argument]\n", int(i), getBytecodeTypeName(ty)); - } - } - else - { - if (ty != LBC_TYPE_ANY) - build.logAppend("; R%d: %s [argument]\n", int(i), getBytecodeTypeName(ty)); + if (const char* name = tryFindLocalName(function.proto, int(i), 0)) + build.logAppend("; R%d: %s [argument '%s']\n", int(i), getBytecodeTypeName_DEPRECATED(ty), name); + else + build.logAppend("; R%d: %s [argument]\n", int(i), getBytecodeTypeName_DEPRECATED(ty)); } } @@ -133,38 +108,76 @@ static void logFunctionTypes(AssemblyBuilder& build, const IrFunction& function) { uint8_t ty = typeInfo.upvalueTypes[i]; - if (FFlag::LuauCodegenIrTypeNames) + if (ty != LBC_TYPE_ANY) { - if (ty != LBC_TYPE_ANY) - { - if (const char* name = tryFindUpvalueName(function.proto, int(i))) - build.logAppend("; U%d: %s ['%s']\n", int(i), getBytecodeTypeName(ty), name); - else - build.logAppend("; U%d: %s\n", int(i), getBytecodeTypeName(ty)); - } - } - else - { - if (ty != LBC_TYPE_ANY) - build.logAppend("; U%d: %s\n", int(i), getBytecodeTypeName(ty)); + if (const char* name = tryFindUpvalueName(function.proto, int(i))) + build.logAppend("; U%d: %s ['%s']\n", int(i), getBytecodeTypeName_DEPRECATED(ty), name); + else + build.logAppend("; U%d: %s\n", int(i), getBytecodeTypeName_DEPRECATED(ty)); } } for (const BytecodeRegTypeInfo& el : typeInfo.regTypes) { - if (FFlag::LuauCodegenIrTypeNames) - { - // Using last active position as the PC because 'startpc' for type info is before local is initialized - if (const char* name = tryFindLocalName(function.proto, el.reg, el.endpc - 1)) - build.logAppend("; R%d: %s from %d to %d [local '%s']\n", el.reg, getBytecodeTypeName(el.type), el.startpc, el.endpc, name); - else - build.logAppend("; R%d: %s from %d to %d\n", el.reg, getBytecodeTypeName(el.type), el.startpc, el.endpc); - } + // Using last active position as the PC because 'startpc' for type info is before local is initialized + if (const char* name = tryFindLocalName(function.proto, el.reg, el.endpc - 1)) + build.logAppend("; R%d: %s from %d to %d [local '%s']\n", el.reg, getBytecodeTypeName_DEPRECATED(el.type), el.startpc, el.endpc, name); else + build.logAppend("; R%d: %s from %d to %d\n", el.reg, getBytecodeTypeName_DEPRECATED(el.type), el.startpc, el.endpc); + } +} + +template +static void logFunctionTypes(AssemblyBuilder& build, const IrFunction& function, const char* const* userdataTypes) +{ + CODEGEN_ASSERT(FFlag::LuauCodegenTypeInfo); + CODEGEN_ASSERT(FFlag::LuauLoadUserdataInfo); + + const BytecodeTypeInfo& typeInfo = function.bcTypeInfo; + + for (size_t i = 0; i < typeInfo.argumentTypes.size(); i++) + { + uint8_t ty = typeInfo.argumentTypes[i]; + + const char* type = getBytecodeTypeName(ty, userdataTypes); + const char* optional = (ty & LBC_TYPE_OPTIONAL_BIT) != 0 ? "?" : ""; + + if (ty != LBC_TYPE_ANY) { - build.logAppend("; R%d: %s from %d to %d\n", el.reg, getBytecodeTypeName(el.type), el.startpc, el.endpc); + if (const char* name = tryFindLocalName(function.proto, int(i), 0)) + build.logAppend("; R%d: %s%s [argument '%s']\n", int(i), type, optional, name); + else + build.logAppend("; R%d: %s%s [argument]\n", int(i), type, optional); } } + + for (size_t i = 0; i < typeInfo.upvalueTypes.size(); i++) + { + uint8_t ty = typeInfo.upvalueTypes[i]; + + const char* type = getBytecodeTypeName(ty, userdataTypes); + const char* optional = (ty & LBC_TYPE_OPTIONAL_BIT) != 0 ? "?" : ""; + + if (ty != LBC_TYPE_ANY) + { + if (const char* name = tryFindUpvalueName(function.proto, int(i))) + build.logAppend("; U%d: %s%s ['%s']\n", int(i), type, optional, name); + else + build.logAppend("; U%d: %s%s\n", int(i), type, optional); + } + } + + for (const BytecodeRegTypeInfo& el : typeInfo.regTypes) + { + const char* type = getBytecodeTypeName(el.type, userdataTypes); + const char* optional = (el.type & LBC_TYPE_OPTIONAL_BIT) != 0 ? "?" : ""; + + // Using last active position as the PC because 'startpc' for type info is before local is initialized + if (const char* name = tryFindLocalName(function.proto, el.reg, el.endpc - 1)) + build.logAppend("; R%d: %s%s from %d to %d [local '%s']\n", el.reg, type, optional, el.startpc, el.endpc, name); + else + build.logAppend("; R%d: %s%s from %d to %d\n", el.reg, type, optional, el.startpc, el.endpc); + } } unsigned getInstructionCount(const Instruction* insns, const unsigned size) @@ -224,7 +237,12 @@ static std::string getAssemblyImpl(AssemblyBuilder& build, const TValue* func, A logFunctionHeader(build, p); if (FFlag::LuauCodegenTypeInfo && options.includeIrTypes) - logFunctionTypes(build, ir.function); + { + if (FFlag::LuauLoadUserdataInfo) + logFunctionTypes(build, ir.function, options.compilationOptions.userdataTypes); + else + logFunctionTypes_DEPRECATED(build, ir.function); + } CodeGenCompilationResult result = CodeGenCompilationResult::Success; diff --git a/CodeGen/src/CodeGenContext.cpp b/CodeGen/src/CodeGenContext.cpp index a94388f6..7788d099 100644 --- a/CodeGen/src/CodeGenContext.cpp +++ b/CodeGen/src/CodeGenContext.cpp @@ -612,5 +612,29 @@ void setNativeExecutionEnabled(lua_State* L, bool enabled) L->global->ecb.enter = enabled ? onEnter : onEnterDisabled; } +static uint8_t userdataRemapperWrap(lua_State* L, const char* str, size_t len) +{ + if (BaseCodeGenContext* codegenCtx = getCodeGenContext(L)) + { + uint8_t index = codegenCtx->userdataRemapper(codegenCtx->userdataRemappingContext, str, len); + + if (index < (LBC_TYPE_TAGGED_USERDATA_END - LBC_TYPE_TAGGED_USERDATA_BASE)) + return LBC_TYPE_TAGGED_USERDATA_BASE + index; + } + + return LBC_TYPE_USERDATA; +} + +void setUserdataRemapper(lua_State* L, void* context, UserdataRemapperCallback cb) +{ + if (BaseCodeGenContext* codegenCtx = getCodeGenContext(L)) + { + codegenCtx->userdataRemappingContext = context; + codegenCtx->userdataRemapper = cb; + + L->global->ecb.gettypemapping = cb ? userdataRemapperWrap : nullptr; + } +} + } // namespace CodeGen } // namespace Luau diff --git a/CodeGen/src/CodeGenContext.h b/CodeGen/src/CodeGenContext.h index 516a7064..43099a9b 100644 --- a/CodeGen/src/CodeGenContext.h +++ b/CodeGen/src/CodeGenContext.h @@ -50,6 +50,9 @@ public: uint8_t* gateData = nullptr; size_t gateDataSize = 0; + void* userdataRemappingContext = nullptr; + UserdataRemapperCallback* userdataRemapper = nullptr; + NativeContext context; }; diff --git a/CodeGen/src/CodeGenLower.h b/CodeGen/src/CodeGenLower.h index efd1034d..6015ef10 100644 --- a/CodeGen/src/CodeGenLower.h +++ b/CodeGen/src/CodeGenLower.h @@ -28,6 +28,7 @@ LUAU_FASTINT(CodegenHeuristicsInstructionLimit) LUAU_FASTINT(CodegenHeuristicsBlockLimit) LUAU_FASTINT(CodegenHeuristicsBlockInstructionLimit) LUAU_FASTFLAG(LuauCodegenRemoveDeadStores5) +LUAU_FASTFLAG(LuauLoadUserdataInfo) namespace Luau { @@ -149,7 +150,11 @@ inline bool lowerImpl(AssemblyBuilder& build, IrLowering& lowering, IrFunction& if (bcTypes.result != LBC_TYPE_ANY || bcTypes.a != LBC_TYPE_ANY || bcTypes.b != LBC_TYPE_ANY || bcTypes.c != LBC_TYPE_ANY) { - toString(ctx.result, bcTypes); + if (FFlag::LuauLoadUserdataInfo) + toString(ctx.result, bcTypes, options.compilationOptions.userdataTypes); + else + toString_DEPRECATED(ctx.result, bcTypes); + build.logAppend("\n"); } } diff --git a/CodeGen/src/IrBuilder.cpp b/CodeGen/src/IrBuilder.cpp index 76d015e9..723d35c4 100644 --- a/CodeGen/src/IrBuilder.cpp +++ b/CodeGen/src/IrBuilder.cpp @@ -14,8 +14,8 @@ #include LUAU_FASTFLAG(LuauLoadTypeInfo) // Because new VM typeinfo load changes the format used by Codegen, same flag is used -LUAU_FASTFLAG(LuauTypeInfoLookupImprovement) LUAU_FASTFLAG(LuauCodegenAnalyzeHostVectorOps) +LUAU_FASTFLAG(LuauLoadUserdataInfo) namespace Luau { @@ -119,20 +119,13 @@ static bool hasTypedParameters(const BytecodeTypeInfo& typeInfo) { CODEGEN_ASSERT(FFlag::LuauLoadTypeInfo); - if (FFlag::LuauTypeInfoLookupImprovement) + for (auto el : typeInfo.argumentTypes) { - for (auto el : typeInfo.argumentTypes) - { - if (el != LBC_TYPE_ANY) - return true; - } + if (el != LBC_TYPE_ANY) + return true; + } - return false; - } - else - { - return !typeInfo.argumentTypes.empty(); - } + return false; } static void buildArgumentTypeChecks(IrBuilder& build) @@ -197,6 +190,19 @@ static void buildArgumentTypeChecks(IrBuilder& build) case LBC_TYPE_BUFFER: build.inst(IrCmd::CHECK_TAG, load, build.constTag(LUA_TBUFFER), build.vmExit(kVmExitEntryGuardPc)); break; + default: + if (FFlag::LuauLoadUserdataInfo) + { + if (tag >= LBC_TYPE_TAGGED_USERDATA_BASE && tag < LBC_TYPE_TAGGED_USERDATA_END) + { + build.inst(IrCmd::CHECK_TAG, load, build.constTag(LUA_TUSERDATA), build.vmExit(kVmExitEntryGuardPc)); + } + else + { + CODEGEN_ASSERT(!"unknown argument type tag"); + } + } + break; } if (optional) diff --git a/CodeGen/src/IrDump.cpp b/CodeGen/src/IrDump.cpp index 48a50ecb..c47a0b8f 100644 --- a/CodeGen/src/IrDump.cpp +++ b/CodeGen/src/IrDump.cpp @@ -7,6 +7,8 @@ #include +LUAU_FASTFLAG(LuauLoadUserdataInfo) + namespace Luau { namespace CodeGen @@ -480,8 +482,10 @@ void toString(std::string& result, IrConst constant) } } -const char* getBytecodeTypeName(uint8_t type) +const char* getBytecodeTypeName_DEPRECATED(uint8_t type) { + CODEGEN_ASSERT(!FFlag::LuauLoadUserdataInfo); + switch (type & ~LBC_TYPE_OPTIONAL_BIT) { case LBC_TYPE_NIL: @@ -512,13 +516,78 @@ const char* getBytecodeTypeName(uint8_t type) return nullptr; } -void toString(std::string& result, const BytecodeTypes& bcTypes) +const char* getBytecodeTypeName(uint8_t type, const char* const* userdataTypes) { + CODEGEN_ASSERT(FFlag::LuauLoadUserdataInfo); + + // Optional bit should be handled externally + type = type & ~LBC_TYPE_OPTIONAL_BIT; + + if (type >= LBC_TYPE_TAGGED_USERDATA_BASE && type < LBC_TYPE_TAGGED_USERDATA_END) + { + if (userdataTypes) + return userdataTypes[type - LBC_TYPE_TAGGED_USERDATA_BASE]; + + return "userdata"; + } + + switch (type) + { + case LBC_TYPE_NIL: + return "nil"; + case LBC_TYPE_BOOLEAN: + return "boolean"; + case LBC_TYPE_NUMBER: + return "number"; + case LBC_TYPE_STRING: + return "string"; + case LBC_TYPE_TABLE: + return "table"; + case LBC_TYPE_FUNCTION: + return "function"; + case LBC_TYPE_THREAD: + return "thread"; + case LBC_TYPE_USERDATA: + return "userdata"; + case LBC_TYPE_VECTOR: + return "vector"; + case LBC_TYPE_BUFFER: + return "buffer"; + case LBC_TYPE_ANY: + return "any"; + } + + CODEGEN_ASSERT(!"Unhandled type in getBytecodeTypeName"); + return nullptr; +} + +void toString_DEPRECATED(std::string& result, const BytecodeTypes& bcTypes) +{ + CODEGEN_ASSERT(!FFlag::LuauLoadUserdataInfo); + if (bcTypes.c != LBC_TYPE_ANY) - append(result, "%s <- %s, %s, %s", getBytecodeTypeName(bcTypes.result), getBytecodeTypeName(bcTypes.a), getBytecodeTypeName(bcTypes.b), - getBytecodeTypeName(bcTypes.c)); + append(result, "%s <- %s, %s, %s", getBytecodeTypeName_DEPRECATED(bcTypes.result), getBytecodeTypeName_DEPRECATED(bcTypes.a), + getBytecodeTypeName_DEPRECATED(bcTypes.b), getBytecodeTypeName_DEPRECATED(bcTypes.c)); else - append(result, "%s <- %s, %s", getBytecodeTypeName(bcTypes.result), getBytecodeTypeName(bcTypes.a), getBytecodeTypeName(bcTypes.b)); + append(result, "%s <- %s, %s", getBytecodeTypeName_DEPRECATED(bcTypes.result), getBytecodeTypeName_DEPRECATED(bcTypes.a), + getBytecodeTypeName_DEPRECATED(bcTypes.b)); +} + +void toString(std::string& result, const BytecodeTypes& bcTypes, const char* const* userdataTypes) +{ + CODEGEN_ASSERT(FFlag::LuauLoadUserdataInfo); + + append(result, "%s%s", getBytecodeTypeName(bcTypes.result, userdataTypes), (bcTypes.result & LBC_TYPE_OPTIONAL_BIT) != 0 ? "?" : ""); + append(result, " <- "); + append(result, "%s%s", getBytecodeTypeName(bcTypes.a, userdataTypes), (bcTypes.a & LBC_TYPE_OPTIONAL_BIT) != 0 ? "?" : ""); + append(result, ", "); + append(result, "%s%s", getBytecodeTypeName(bcTypes.b, userdataTypes), (bcTypes.b & LBC_TYPE_OPTIONAL_BIT) != 0 ? "?" : ""); + + if (bcTypes.c != LBC_TYPE_ANY) + { + append(result, ", "); + append(result, "%s%s", getBytecodeTypeName(bcTypes.c, userdataTypes), (bcTypes.c & LBC_TYPE_OPTIONAL_BIT) != 0 ? "?" : ""); + } } static void appendBlockSet(IrToStringContext& ctx, BlockIteratorWrapper blocks) diff --git a/CodeGen/src/IrTranslation.cpp b/CodeGen/src/IrTranslation.cpp index 291f618b..93073a92 100644 --- a/CodeGen/src/IrTranslation.cpp +++ b/CodeGen/src/IrTranslation.cpp @@ -14,7 +14,6 @@ #include "ltm.h" LUAU_FASTFLAGVARIABLE(LuauCodegenDirectUserdataFlow, false) -LUAU_FASTFLAGVARIABLE(LuauCodegenFixVectorFields, false) LUAU_FASTFLAG(LuauCodegenAnalyzeHostVectorOps) namespace Luau @@ -1200,19 +1199,19 @@ void translateInstGetTableKS(IrBuilder& build, const Instruction* pc, int pcpos) TString* str = gco2ts(build.function.proto->k[aux].value.gc); const char* field = getstr(str); - if ((!FFlag::LuauCodegenFixVectorFields || str->len == 1) && (*field == 'X' || *field == 'x')) + if (str->len == 1 && (*field == 'X' || *field == 'x')) { IrOp value = build.inst(IrCmd::LOAD_FLOAT, build.vmReg(rb), build.constInt(0)); build.inst(IrCmd::STORE_DOUBLE, build.vmReg(ra), value); build.inst(IrCmd::STORE_TAG, build.vmReg(ra), build.constTag(LUA_TNUMBER)); } - else if ((!FFlag::LuauCodegenFixVectorFields || str->len == 1) && (*field == 'Y' || *field == 'y')) + else if (str->len == 1 && (*field == 'Y' || *field == 'y')) { IrOp value = build.inst(IrCmd::LOAD_FLOAT, build.vmReg(rb), build.constInt(4)); build.inst(IrCmd::STORE_DOUBLE, build.vmReg(ra), value); build.inst(IrCmd::STORE_TAG, build.vmReg(ra), build.constTag(LUA_TNUMBER)); } - else if ((!FFlag::LuauCodegenFixVectorFields || str->len == 1) && (*field == 'Z' || *field == 'z')) + else if (str->len == 1 && (*field == 'Z' || *field == 'z')) { IrOp value = build.inst(IrCmd::LOAD_FLOAT, build.vmReg(rb), build.constInt(8)); build.inst(IrCmd::STORE_DOUBLE, build.vmReg(ra), value); diff --git a/CodeGen/src/IrUtils.cpp b/CodeGen/src/IrUtils.cpp index caa6b178..afc6ba5a 100644 --- a/CodeGen/src/IrUtils.cpp +++ b/CodeGen/src/IrUtils.cpp @@ -252,6 +252,16 @@ bool isGCO(uint8_t tag) return tag >= LUA_TSTRING; } +bool isUserdataBytecodeType(uint8_t ty) +{ + return ty == LBC_TYPE_USERDATA || isCustomUserdataBytecodeType(ty); +} + +bool isCustomUserdataBytecodeType(uint8_t ty) +{ + return ty >= LBC_TYPE_TAGGED_USERDATA_BASE && ty < LBC_TYPE_TAGGED_USERDATA_END; +} + void kill(IrFunction& function, IrInst& inst) { CODEGEN_ASSERT(inst.useCount == 0); diff --git a/CodeGen/src/OptimizeConstProp.cpp b/CodeGen/src/OptimizeConstProp.cpp index eae0baa3..9135a9ed 100644 --- a/CodeGen/src/OptimizeConstProp.cpp +++ b/CodeGen/src/OptimizeConstProp.cpp @@ -18,7 +18,7 @@ LUAU_FASTINTVARIABLE(LuauCodeGenMinLinearBlockPath, 3) LUAU_FASTINTVARIABLE(LuauCodeGenReuseSlotLimit, 64) LUAU_FASTFLAGVARIABLE(DebugLuauAbortingChecks, false) LUAU_FASTFLAG(LuauCodegenRemoveDeadStores5) -LUAU_FASTFLAGVARIABLE(LuauCodegenLoadPropCheckRegLinkInTv, false) +LUAU_FASTFLAGVARIABLE(LuauCodegenFixSplitStoreConstMismatch, false) namespace Luau { @@ -739,7 +739,7 @@ static void constPropInInst(ConstPropState& state, IrBuilder& build, IrFunction& // If we know the tag, we can try extracting the value from a register used by LOAD_TVALUE // To do that, we have to ensure that the register link of the source value is still valid - if (tag != 0xff && (!FFlag::LuauCodegenLoadPropCheckRegLinkInTv || state.tryGetRegLink(inst.b) != nullptr)) + if (tag != 0xff && state.tryGetRegLink(inst.b) != nullptr) { if (IrInst* arg = function.asInstOp(inst.b); arg && arg->cmd == IrCmd::LOAD_TVALUE && arg->a.kind == IrOpKind::VmReg) { @@ -750,18 +750,48 @@ static void constPropInInst(ConstPropState& state, IrBuilder& build, IrFunction& } } - // If we have constant tag and value, replace TValue store with tag/value pair store - if (tag != 0xff && value.kind != IrOpKind::None && (tag == LUA_TBOOLEAN || tag == LUA_TNUMBER || isGCO(tag))) + if (FFlag::LuauCodegenFixSplitStoreConstMismatch) { - replace(function, block, index, {IrCmd::STORE_SPLIT_TVALUE, inst.a, build.constTag(tag), value, inst.c}); + // If we have constant tag and value, replace TValue store with tag/value pair store + bool canSplitTvalueStore = false; - // Value can be propagated to future loads of the same register - if (inst.a.kind == IrOpKind::VmReg && activeLoadValue != kInvalidInstIdx) - state.valueMap[state.versionedVmRegLoad(activeLoadCmd, inst.a)] = activeLoadValue; + if (tag == LUA_TBOOLEAN && + (value.kind == IrOpKind::Inst || (value.kind == IrOpKind::Constant && function.constOp(value).kind == IrConstKind::Int))) + canSplitTvalueStore = true; + else if (tag == LUA_TNUMBER && + (value.kind == IrOpKind::Inst || (value.kind == IrOpKind::Constant && function.constOp(value).kind == IrConstKind::Double))) + canSplitTvalueStore = true; + else if (tag != 0xff && isGCO(tag) && value.kind == IrOpKind::Inst) + canSplitTvalueStore = true; + + if (canSplitTvalueStore) + { + replace(function, block, index, {IrCmd::STORE_SPLIT_TVALUE, inst.a, build.constTag(tag), value, inst.c}); + + // Value can be propagated to future loads of the same register + if (inst.a.kind == IrOpKind::VmReg && activeLoadValue != kInvalidInstIdx) + state.valueMap[state.versionedVmRegLoad(activeLoadCmd, inst.a)] = activeLoadValue; + } + else if (inst.a.kind == IrOpKind::VmReg) + { + state.forwardVmRegStoreToLoad(inst, IrCmd::LOAD_TVALUE); + } } - else if (inst.a.kind == IrOpKind::VmReg) + else { - state.forwardVmRegStoreToLoad(inst, IrCmd::LOAD_TVALUE); + // If we have constant tag and value, replace TValue store with tag/value pair store + if (tag != 0xff && value.kind != IrOpKind::None && (tag == LUA_TBOOLEAN || tag == LUA_TNUMBER || isGCO(tag))) + { + replace(function, block, index, {IrCmd::STORE_SPLIT_TVALUE, inst.a, build.constTag(tag), value, inst.c}); + + // Value can be propagated to future loads of the same register + if (inst.a.kind == IrOpKind::VmReg && activeLoadValue != kInvalidInstIdx) + state.valueMap[state.versionedVmRegLoad(activeLoadCmd, inst.a)] = activeLoadValue; + } + else if (inst.a.kind == IrOpKind::VmReg) + { + state.forwardVmRegStoreToLoad(inst, IrCmd::LOAD_TVALUE); + } } } break; diff --git a/Common/include/Luau/Bytecode.h b/Common/include/Luau/Bytecode.h index 7012d820..2ae54c67 100644 --- a/Common/include/Luau/Bytecode.h +++ b/Common/include/Luau/Bytecode.h @@ -438,7 +438,9 @@ enum LuauBytecodeTag LBC_VERSION_TARGET = 5, // Type encoding version LBC_TYPE_VERSION_DEPRECATED = 1, - LBC_TYPE_VERSION = 2, + LBC_TYPE_VERSION_MIN = 1, + LBC_TYPE_VERSION_MAX = 3, + LBC_TYPE_VERSION_TARGET = 3, // Types of constant table entries LBC_CONSTANT_NIL = 0, LBC_CONSTANT_BOOLEAN, @@ -465,6 +467,10 @@ enum LuauBytecodeType LBC_TYPE_BUFFER, LBC_TYPE_ANY = 15, + + LBC_TYPE_TAGGED_USERDATA_BASE = 64, + LBC_TYPE_TAGGED_USERDATA_END = 64 + 32, + LBC_TYPE_OPTIONAL_BIT = 1 << 7, LBC_TYPE_INVALID = 256, diff --git a/Compiler/include/Luau/BytecodeBuilder.h b/Compiler/include/Luau/BytecodeBuilder.h index 7f0115bb..59d30d62 100644 --- a/Compiler/include/Luau/BytecodeBuilder.h +++ b/Compiler/include/Luau/BytecodeBuilder.h @@ -79,6 +79,9 @@ public: void pushLocalTypeInfo(LuauBytecodeType type, uint8_t reg, uint32_t startpc, uint32_t endpc); void pushUpvalTypeInfo(LuauBytecodeType type); + uint32_t addUserdataType(const char* name); + void useUserdataType(uint32_t index); + void setDebugFunctionName(StringRef name); void setDebugFunctionLineDefined(int line); void setDebugLine(int line); @@ -229,6 +232,13 @@ private: LuauBytecodeType type; }; + struct UserdataType + { + std::string name; + uint32_t nameRef = 0; + bool used = false; + }; + struct Jump { uint32_t source; @@ -277,6 +287,8 @@ private: std::vector typedLocals; std::vector typedUpvals; + std::vector userdataTypes; + DenseHashMap stringTable; std::vector debugStrings; @@ -308,6 +320,8 @@ private: int32_t addConstant(const ConstantKey& key, const Constant& value); unsigned int addStringTableEntry(StringRef value); + + const char* tryGetUserdataTypeName(LuauBytecodeType type) const; }; } // namespace Luau diff --git a/Compiler/include/Luau/Compiler.h b/Compiler/include/Luau/Compiler.h index 698a50c4..119e0aa2 100644 --- a/Compiler/include/Luau/Compiler.h +++ b/Compiler/include/Luau/Compiler.h @@ -46,6 +46,9 @@ struct CompileOptions // null-terminated array of globals that are mutable; disables the import optimization for fields accessed through these const char* const* mutableGlobals = nullptr; + + // null-terminated array of userdata types that will be included in the type information + const char* const* userdataTypes = nullptr; }; class CompileError : public std::exception diff --git a/Compiler/include/luacode.h b/Compiler/include/luacode.h index a470319d..1d200817 100644 --- a/Compiler/include/luacode.h +++ b/Compiler/include/luacode.h @@ -42,6 +42,9 @@ struct lua_CompileOptions // null-terminated array of globals that are mutable; disables the import optimization for fields accessed through these const char* const* mutableGlobals; + + // null-terminated array of userdata types that will be included in the type information + const char* const* userdataTypes = nullptr; }; // compile source to bytecode; when source compilation fails, the resulting bytecode contains the encoded error. use free() to destroy diff --git a/Compiler/src/BytecodeBuilder.cpp b/Compiler/src/BytecodeBuilder.cpp index 6c76b671..59aee1e7 100644 --- a/Compiler/src/BytecodeBuilder.cpp +++ b/Compiler/src/BytecodeBuilder.cpp @@ -7,9 +7,8 @@ #include #include -LUAU_FASTFLAGVARIABLE(LuauCompileNoJumpLineRetarget, false) -LUAU_FASTFLAG(LuauCompileRepeatUntilSkippedLocals) LUAU_FASTFLAGVARIABLE(LuauCompileTypeInfo, false) +LUAU_FASTFLAG(LuauCompileUserdataInfo) namespace Luau { @@ -335,6 +334,18 @@ unsigned int BytecodeBuilder::addStringTableEntry(StringRef value) return index; } +const char* BytecodeBuilder::tryGetUserdataTypeName(LuauBytecodeType type) const +{ + LUAU_ASSERT(FFlag::LuauCompileUserdataInfo); + + unsigned index = unsigned((type & ~LBC_TYPE_OPTIONAL_BIT) - LBC_TYPE_TAGGED_USERDATA_BASE); + + if (index < userdataTypes.size()) + return userdataTypes[index].name.c_str(); + + return nullptr; +} + int32_t BytecodeBuilder::addConstantNil() { Constant c = {Constant::Type_Nil}; @@ -567,6 +578,25 @@ void BytecodeBuilder::pushUpvalTypeInfo(LuauBytecodeType type) typedUpvals.push_back(upval); } +uint32_t BytecodeBuilder::addUserdataType(const char* name) +{ + LUAU_ASSERT(FFlag::LuauCompileUserdataInfo); + + UserdataType ty; + + ty.name = name; + + userdataTypes.push_back(std::move(ty)); + return uint32_t(userdataTypes.size() - 1); +} + +void BytecodeBuilder::useUserdataType(uint32_t index) +{ + LUAU_ASSERT(FFlag::LuauCompileUserdataInfo); + + userdataTypes[index].used = true; +} + void BytecodeBuilder::setDebugFunctionName(StringRef name) { unsigned int index = addStringTableEntry(name); @@ -648,6 +678,15 @@ void BytecodeBuilder::finalize() { LUAU_ASSERT(bytecode.empty()); + if (FFlag::LuauCompileUserdataInfo) + { + for (auto& ty : userdataTypes) + { + if (ty.used) + ty.nameRef = addStringTableEntry(StringRef({ty.name.c_str(), ty.name.length()})); + } + } + // preallocate space for bytecode blob size_t capacity = 16; @@ -666,10 +705,24 @@ void BytecodeBuilder::finalize() bytecode = char(version); uint8_t typesversion = getTypeEncodingVersion(); + LUAU_ASSERT(typesversion >= LBC_TYPE_VERSION_MIN && typesversion <= LBC_TYPE_VERSION_MAX); writeByte(bytecode, typesversion); writeStringTable(bytecode); + if (FFlag::LuauCompileTypeInfo && FFlag::LuauCompileUserdataInfo) + { + // Write the mapping between used type name indices and their name + for (uint32_t i = 0; i < uint32_t(userdataTypes.size()); i++) + { + writeByte(bytecode, i + 1); + writeVarInt(bytecode, userdataTypes[i].nameRef); + } + + // 0 marks the end of the mapping + writeByte(bytecode, 0); + } + writeVarInt(bytecode, uint32_t(functions.size())); for (const Function& func : functions) @@ -1036,11 +1089,6 @@ void BytecodeBuilder::foldJumps() if (LUAU_INSN_OP(jumpInsn) == LOP_JUMP && LUAU_INSN_OP(targetInsn) == LOP_RETURN) { insns[jumpLabel] = targetInsn; - - if (!FFlag::LuauCompileNoJumpLineRetarget) - { - lines[jumpLabel] = lines[targetLabel]; - } } else if (int16_t(offset) == offset) { @@ -1198,7 +1246,10 @@ uint8_t BytecodeBuilder::getVersion() uint8_t BytecodeBuilder::getTypeEncodingVersion() { - return FFlag::LuauCompileTypeInfo ? LBC_TYPE_VERSION : LBC_TYPE_VERSION_DEPRECATED; + if (FFlag::LuauCompileTypeInfo && FFlag::LuauCompileUserdataInfo) + return LBC_TYPE_VERSION_TARGET; + + return FFlag::LuauCompileTypeInfo ? 2 : LBC_TYPE_VERSION_DEPRECATED; } #ifdef LUAU_ASSERTENABLED @@ -2275,7 +2326,7 @@ std::string BytecodeBuilder::dumpCurrentFunction(std::vector& dumpinstoffs) { const DebugLocal& l = debugLocals[i]; - if (FFlag::LuauCompileRepeatUntilSkippedLocals && l.startpc == l.endpc) + if (l.startpc == l.endpc) { LUAU_ASSERT(l.startpc < lines.size()); @@ -2301,35 +2352,74 @@ std::string BytecodeBuilder::dumpCurrentFunction(std::vector& dumpinstoffs) { const std::string& typeinfo = functions.back().typeinfo; - // Arguments start from third byte in function typeinfo string - for (uint8_t i = 2; i < typeinfo.size(); ++i) + if (FFlag::LuauCompileUserdataInfo) { - uint8_t et = typeinfo[i]; + // Arguments start from third byte in function typeinfo string + for (uint8_t i = 2; i < typeinfo.size(); ++i) + { + uint8_t et = typeinfo[i]; - const char* base = getBaseTypeString(et); - const char* optional = (et & LBC_TYPE_OPTIONAL_BIT) ? "?" : ""; + const char* userdata = tryGetUserdataTypeName(LuauBytecodeType(et)); + const char* name = userdata ? userdata : getBaseTypeString(et); + const char* optional = (et & LBC_TYPE_OPTIONAL_BIT) ? "?" : ""; - formatAppend(result, "R%d: %s%s [argument]\n", i - 2, base, optional); + formatAppend(result, "R%d: %s%s [argument]\n", i - 2, name, optional); + } + + for (size_t i = 0; i < typedUpvals.size(); ++i) + { + const TypedUpval& l = typedUpvals[i]; + + const char* userdata = tryGetUserdataTypeName(l.type); + const char* name = userdata ? userdata : getBaseTypeString(l.type); + const char* optional = (l.type & LBC_TYPE_OPTIONAL_BIT) ? "?" : ""; + + formatAppend(result, "U%d: %s%s\n", int(i), name, optional); + } + + for (size_t i = 0; i < typedLocals.size(); ++i) + { + const TypedLocal& l = typedLocals[i]; + + const char* userdata = tryGetUserdataTypeName(l.type); + const char* name = userdata ? userdata : getBaseTypeString(l.type); + const char* optional = (l.type & LBC_TYPE_OPTIONAL_BIT) ? "?" : ""; + + formatAppend(result, "R%d: %s%s from %d to %d\n", l.reg, name, optional, l.startpc, l.endpc); + } } - - for (size_t i = 0; i < typedUpvals.size(); ++i) + else { - const TypedUpval& l = typedUpvals[i]; + // Arguments start from third byte in function typeinfo string + for (uint8_t i = 2; i < typeinfo.size(); ++i) + { + uint8_t et = typeinfo[i]; - const char* base = getBaseTypeString(l.type); - const char* optional = (l.type & LBC_TYPE_OPTIONAL_BIT) ? "?" : ""; + const char* base = getBaseTypeString(et); + const char* optional = (et & LBC_TYPE_OPTIONAL_BIT) ? "?" : ""; - formatAppend(result, "U%d: %s%s\n", int(i), base, optional); - } + formatAppend(result, "R%d: %s%s [argument]\n", i - 2, base, optional); + } - for (size_t i = 0; i < typedLocals.size(); ++i) - { - const TypedLocal& l = typedLocals[i]; + for (size_t i = 0; i < typedUpvals.size(); ++i) + { + const TypedUpval& l = typedUpvals[i]; - const char* base = getBaseTypeString(l.type); - const char* optional = (l.type & LBC_TYPE_OPTIONAL_BIT) ? "?" : ""; + const char* base = getBaseTypeString(l.type); + const char* optional = (l.type & LBC_TYPE_OPTIONAL_BIT) ? "?" : ""; - formatAppend(result, "R%d: %s%s from %d to %d\n", l.reg, base, optional, l.startpc, l.endpc); + formatAppend(result, "U%d: %s%s\n", int(i), base, optional); + } + + for (size_t i = 0; i < typedLocals.size(); ++i) + { + const TypedLocal& l = typedLocals[i]; + + const char* base = getBaseTypeString(l.type); + const char* optional = (l.type & LBC_TYPE_OPTIONAL_BIT) ? "?" : ""; + + formatAppend(result, "R%d: %s%s from %d to %d\n", l.reg, base, optional, l.startpc, l.endpc); + } } } } diff --git a/Compiler/src/Compiler.cpp b/Compiler/src/Compiler.cpp index d5cd78a5..19526fa9 100644 --- a/Compiler/src/Compiler.cpp +++ b/Compiler/src/Compiler.cpp @@ -26,10 +26,9 @@ LUAU_FASTINTVARIABLE(LuauCompileInlineThreshold, 25) LUAU_FASTINTVARIABLE(LuauCompileInlineThresholdMaxBoost, 300) LUAU_FASTINTVARIABLE(LuauCompileInlineDepth, 5) -LUAU_FASTFLAGVARIABLE(LuauCompileRepeatUntilSkippedLocals, false) LUAU_FASTFLAG(LuauCompileTypeInfo) -LUAU_FASTFLAGVARIABLE(LuauTypeInfoLookupImprovement, false) LUAU_FASTFLAGVARIABLE(LuauCompileTempTypeInfo, false) +LUAU_FASTFLAGVARIABLE(LuauCompileUserdataInfo, false) namespace Luau { @@ -107,6 +106,7 @@ struct Compiler , locstants(nullptr) , tableShapes(nullptr) , builtins(nullptr) + , userdataTypes(AstName()) , functionTypes(nullptr) , localTypes(nullptr) , exprTypes(nullptr) @@ -677,10 +677,7 @@ struct Compiler // if the argument is a local that isn't mutated, we will simply reuse the existing register if (int reg = le ? getExprLocalReg(le) : -1; reg >= 0 && (!lv || !lv->written)) { - if (FFlag::LuauTypeInfoLookupImprovement) - args.push_back({var, uint8_t(reg), {Constant::Type_Unknown}, kDefaultAllocPc}); - else - args.push_back({var, uint8_t(reg)}); + args.push_back({var, uint8_t(reg), {Constant::Type_Unknown}, kDefaultAllocPc}); } else { @@ -2771,16 +2768,14 @@ struct Compiler { validateContinueUntil(loops.back().continueUsed, stat->condition, body, i + 1); continueValidated = true; - - if (FFlag::LuauCompileRepeatUntilSkippedLocals) - conditionLocals = localStack.size(); + conditionLocals = localStack.size(); } } // if continue was used, some locals might not have had their initialization completed // the lifetime of these locals has to end before the condition is executed // because referencing skipped locals is not possible from the condition, this earlier closure doesn't affect upvalues - if (FFlag::LuauCompileRepeatUntilSkippedLocals && continueValidated) + if (continueValidated) { // if continueValidated is set, it means we have visited at least one body node and size > 0 setDebugLineEnd(body->body.data[body->body.size - 1]); @@ -4094,6 +4089,7 @@ struct Compiler DenseHashMap locstants; DenseHashMap tableShapes; DenseHashMap builtins; + DenseHashMap userdataTypes; DenseHashMap functionTypes; DenseHashMap localTypes; DenseHashMap exprTypes; @@ -4190,18 +4186,34 @@ void compileOrThrow(BytecodeBuilder& bytecode, const ParseResult& parseResult, c Compiler::FunctionVisitor functionVisitor(&compiler, functions); root->visit(&functionVisitor); + if (FFlag::LuauCompileUserdataInfo) + { + if (const char* const* ptr = options.userdataTypes) + { + for (; *ptr; ++ptr) + { + // Type will only resolve to an AstName if it is actually mentioned in the source + if (AstName name = names.get(*ptr); name.value) + compiler.userdataTypes[name] = bytecode.addUserdataType(name.value); + } + + if (uintptr_t(ptr - options.userdataTypes) > (LBC_TYPE_TAGGED_USERDATA_END - LBC_TYPE_TAGGED_USERDATA_BASE)) + CompileError::raise(root->location, "Exceeded userdata type limit in the compilation options"); + } + } + // computes type information for all functions based on type annotations if (FFlag::LuauCompileTypeInfo) { if (options.typeInfoLevel >= 1) - buildTypeMap(compiler.functionTypes, compiler.localTypes, compiler.exprTypes, root, options.vectorType, compiler.builtinTypes, - compiler.builtins, compiler.globals); + buildTypeMap(compiler.functionTypes, compiler.localTypes, compiler.exprTypes, root, options.vectorType, compiler.userdataTypes, + compiler.builtinTypes, compiler.builtins, compiler.globals, bytecode); } else { if (functionVisitor.hasTypes) - buildTypeMap(compiler.functionTypes, compiler.localTypes, compiler.exprTypes, root, options.vectorType, compiler.builtinTypes, - compiler.builtins, compiler.globals); + buildTypeMap(compiler.functionTypes, compiler.localTypes, compiler.exprTypes, root, options.vectorType, compiler.userdataTypes, + compiler.builtinTypes, compiler.builtins, compiler.globals, bytecode); } for (AstExprFunction* expr : functions) diff --git a/Compiler/src/Types.cpp b/Compiler/src/Types.cpp index eaa2d8be..4454114c 100644 --- a/Compiler/src/Types.cpp +++ b/Compiler/src/Types.cpp @@ -5,6 +5,7 @@ LUAU_FASTFLAG(LuauCompileTypeInfo) LUAU_FASTFLAG(LuauCompileTempTypeInfo) +LUAU_FASTFLAG(LuauCompileUserdataInfo) namespace Luau { @@ -39,7 +40,8 @@ static LuauBytecodeType getPrimitiveType(AstName name) } static LuauBytecodeType getType(const AstType* ty, const AstArray& generics, - const DenseHashMap& typeAliases, bool resolveAliases, const char* vectorType) + const DenseHashMap& typeAliases, bool resolveAliases, const char* vectorType, + const DenseHashMap& userdataTypes, BytecodeBuilder& bytecode) { if (const AstTypeReference* ref = ty->as()) { @@ -50,7 +52,7 @@ static LuauBytecodeType getType(const AstType* ty, const AstArraytype, (*alias)->generics, typeAliases, /* resolveAliases= */ false, vectorType); + return getType((*alias)->type, (*alias)->generics, typeAliases, /* resolveAliases= */ false, vectorType, userdataTypes, bytecode); else return LBC_TYPE_ANY; } @@ -64,6 +66,15 @@ static LuauBytecodeType getType(const AstType* ty, const AstArrayname); prim != LBC_TYPE_INVALID) return prim; + if (FFlag::LuauCompileUserdataInfo) + { + if (const uint8_t* userdataIndex = userdataTypes.find(ref->name)) + { + bytecode.useUserdataType(*userdataIndex); + return LuauBytecodeType(LBC_TYPE_TAGGED_USERDATA_BASE + *userdataIndex); + } + } + // not primitive or alias or generic => host-provided, we assume userdata for now return LBC_TYPE_USERDATA; } @@ -82,7 +93,7 @@ static LuauBytecodeType getType(const AstType* ty, const AstArraytypes) { - LuauBytecodeType et = getType(ty, generics, typeAliases, resolveAliases, vectorType); + LuauBytecodeType et = getType(ty, generics, typeAliases, resolveAliases, vectorType, userdataTypes, bytecode); if (et == LBC_TYPE_NIL) { @@ -113,7 +124,8 @@ static LuauBytecodeType getType(const AstType* ty, const AstArray& typeAliases, const char* vectorType) +static std::string getFunctionType(const AstExprFunction* func, const DenseHashMap& typeAliases, const char* vectorType, + const DenseHashMap& userdataTypes, BytecodeBuilder& bytecode) { bool self = func->self != 0; @@ -130,7 +142,8 @@ static std::string getFunctionType(const AstExprFunction* func, const DenseHashM for (AstLocal* arg : func->args) { LuauBytecodeType ty = - arg->annotation ? getType(arg->annotation, func->generics, typeAliases, /* resolveAliases= */ true, vectorType) : LBC_TYPE_ANY; + arg->annotation ? getType(arg->annotation, func->generics, typeAliases, /* resolveAliases= */ true, vectorType, userdataTypes, bytecode) + : LBC_TYPE_ANY; if (ty != LBC_TYPE_ANY) haveNonAnyParam = true; @@ -161,9 +174,11 @@ struct TypeMapVisitor : AstVisitor DenseHashMap& localTypes; DenseHashMap& exprTypes; const char* vectorType; + const DenseHashMap& userdataTypes; const BuiltinTypes& builtinTypes; const DenseHashMap& builtinCalls; const DenseHashMap& globals; + BytecodeBuilder& bytecode; DenseHashMap typeAliases; std::vector> typeAliasStack; @@ -171,15 +186,18 @@ struct TypeMapVisitor : AstVisitor DenseHashMap resolvedExprs; TypeMapVisitor(DenseHashMap& functionTypes, DenseHashMap& localTypes, - DenseHashMap& exprTypes, const char* vectorType, const BuiltinTypes& builtinTypes, - const DenseHashMap& builtinCalls, const DenseHashMap& globals) + DenseHashMap& exprTypes, const char* vectorType, const DenseHashMap& userdataTypes, + const BuiltinTypes& builtinTypes, const DenseHashMap& builtinCalls, const DenseHashMap& globals, + BytecodeBuilder& bytecode) : functionTypes(functionTypes) , localTypes(localTypes) , exprTypes(exprTypes) , vectorType(vectorType) + , userdataTypes(userdataTypes) , builtinTypes(builtinTypes) , builtinCalls(builtinCalls) , globals(globals) + , bytecode(bytecode) , typeAliases(AstName()) , resolvedLocals(nullptr) , resolvedExprs(nullptr) @@ -250,7 +268,7 @@ struct TypeMapVisitor : AstVisitor resolvedExprs[expr] = ty; - LuauBytecodeType bty = getType(ty, {}, typeAliases, /* resolveAliases= */ true, vectorType); + LuauBytecodeType bty = getType(ty, {}, typeAliases, /* resolveAliases= */ true, vectorType, userdataTypes, bytecode); exprTypes[expr] = bty; return bty; } @@ -263,7 +281,7 @@ struct TypeMapVisitor : AstVisitor resolvedLocals[local] = ty; - LuauBytecodeType bty = getType(ty, {}, typeAliases, /* resolveAliases= */ true, vectorType); + LuauBytecodeType bty = getType(ty, {}, typeAliases, /* resolveAliases= */ true, vectorType, userdataTypes, bytecode); if (bty != LBC_TYPE_ANY) localTypes[local] = bty; @@ -354,7 +372,7 @@ struct TypeMapVisitor : AstVisitor bool visit(AstExprFunction* node) override { - std::string type = getFunctionType(node, typeAliases, vectorType); + std::string type = getFunctionType(node, typeAliases, vectorType, userdataTypes, bytecode); if (!type.empty()) functionTypes[node] = std::move(type); @@ -393,7 +411,7 @@ struct TypeMapVisitor : AstVisitor if (AstType* annotation = local->annotation) { - LuauBytecodeType ty = getType(annotation, {}, typeAliases, /* resolveAliases= */ true, vectorType); + LuauBytecodeType ty = getType(annotation, {}, typeAliases, /* resolveAliases= */ true, vectorType, userdataTypes, bytecode); if (ty != LBC_TYPE_ANY) localTypes[local] = ty; @@ -754,10 +772,11 @@ struct TypeMapVisitor : AstVisitor }; void buildTypeMap(DenseHashMap& functionTypes, DenseHashMap& localTypes, - DenseHashMap& exprTypes, AstNode* root, const char* vectorType, const BuiltinTypes& builtinTypes, - const DenseHashMap& builtinCalls, const DenseHashMap& globals) + DenseHashMap& exprTypes, AstNode* root, const char* vectorType, const DenseHashMap& userdataTypes, + const BuiltinTypes& builtinTypes, const DenseHashMap& builtinCalls, const DenseHashMap& globals, + BytecodeBuilder& bytecode) { - TypeMapVisitor visitor(functionTypes, localTypes, exprTypes, vectorType, builtinTypes, builtinCalls, globals); + TypeMapVisitor visitor(functionTypes, localTypes, exprTypes, vectorType, userdataTypes, builtinTypes, builtinCalls, globals, bytecode); root->visit(&visitor); } diff --git a/Compiler/src/Types.h b/Compiler/src/Types.h index b1aff8a2..bd12ea77 100644 --- a/Compiler/src/Types.h +++ b/Compiler/src/Types.h @@ -10,6 +10,7 @@ namespace Luau { +class BytecodeBuilder; struct BuiltinTypes { @@ -26,7 +27,8 @@ struct BuiltinTypes }; void buildTypeMap(DenseHashMap& functionTypes, DenseHashMap& localTypes, - DenseHashMap& exprTypes, AstNode* root, const char* vectorType, const BuiltinTypes& builtinTypes, - const DenseHashMap& builtinCalls, const DenseHashMap& globals); + DenseHashMap& exprTypes, AstNode* root, const char* vectorType, const DenseHashMap& userdataTypes, + const BuiltinTypes& builtinTypes, const DenseHashMap& builtinCalls, const DenseHashMap& globals, + BytecodeBuilder& bytecode); } // namespace Luau diff --git a/VM/src/lstate.h b/VM/src/lstate.h index 21d7071c..35e66471 100644 --- a/VM/src/lstate.h +++ b/VM/src/lstate.h @@ -156,6 +156,7 @@ struct lua_ExecutionCallbacks int (*enter)(lua_State* L, Proto* proto); // called when function is about to start/resume (when execdata is present), return 0 to exit VM void (*disable)(lua_State* L, Proto* proto); // called when function has to be switched from native to bytecode in the debugger size_t (*getmemorysize)(lua_State* L, Proto* proto); // called to request the size of memory associated with native part of the Proto + uint8_t (*gettypemapping)(lua_State* L, const char* str, size_t len); // called to get the userdata type index }; /* diff --git a/VM/src/ltablib.cpp b/VM/src/ltablib.cpp index 545c1d2d..3e14d4ad 100644 --- a/VM/src/ltablib.cpp +++ b/VM/src/ltablib.cpp @@ -11,7 +11,6 @@ #include "ldebug.h" #include "lvm.h" -LUAU_DYNAMIC_FASTFLAGVARIABLE(LuauFastCrossTableMove, false) LUAU_DYNAMIC_FASTFLAGVARIABLE(LuauFastTableMaxn, false) LUAU_DYNAMIC_FASTFLAGVARIABLE(LuauFasterConcat, false) @@ -145,68 +144,6 @@ static void moveelements(lua_State* L, int srct, int dstt, int f, int e, int t) luaC_barrierfast(L, dst); } - else if (DFFlag::LuauFastCrossTableMove && dst != src) - { - // compute the array slice we have to copy over - int slicestart = f < 1 ? 0 : (f > src->sizearray ? src->sizearray : f - 1); - int sliceend = e < 1 ? 0 : (e > src->sizearray ? src->sizearray : e); - LUAU_ASSERT(slicestart <= sliceend); - - int slicecount = sliceend - slicestart; - - if (slicecount > 0) - { - // array slice starting from INT_MIN is impossible, so we don't have to worry about int overflow - int dstslicestart = f < 1 ? -f + 1 : 0; - - // copy over the slice - for (int i = 0; i < slicecount; ++i) - { - lua_rawgeti(L, srct, slicestart + i + 1); - lua_rawseti(L, dstt, dstslicestart + t + i); - } - } - - // copy the remaining elements that could be in the hash part - int hashpartsize = sizenode(src); - - // select the strategy with the least amount of steps - if (n <= hashpartsize) - { - for (int i = 0; i < n; ++i) - { - // skip array slice elements that were already copied over - if (cast_to(unsigned int, f + i - 1) < cast_to(unsigned int, src->sizearray)) - continue; - - lua_rawgeti(L, srct, f + i); - lua_rawseti(L, dstt, t + i); - } - } - else - { - // source and destination tables are different, so we can iterate over source hash part directly - int i = hashpartsize; - - while (i--) - { - LuaNode* node = gnode(src, i); - if (ttisnumber(gkey(node))) - { - double n = nvalue(gkey(node)); - - int k; - luai_num2int(k, n); - - if (luai_numeq(cast_num(k), n) && k >= f && k <= e) - { - lua_rawgeti(L, srct, k); - lua_rawseti(L, dstt, t - f + k); - } - } - } - } - } else { if (t > e || t <= f || dst != src) diff --git a/VM/src/lvmload.cpp b/VM/src/lvmload.cpp index f13c0f21..ed564bba 100644 --- a/VM/src/lvmload.cpp +++ b/VM/src/lvmload.cpp @@ -14,6 +14,7 @@ #include LUAU_FASTFLAG(LuauLoadTypeInfo) +LUAU_FASTFLAGVARIABLE(LuauLoadUserdataInfo, false) // TODO: RAII deallocation doesn't work for longjmp builds if a memory error happens template @@ -187,6 +188,65 @@ static void resolveImportSafe(lua_State* L, Table* env, TValue* k, uint32_t id) } } +static void remapUserdataTypes(char* data, size_t size, uint8_t* userdataRemapping, uint32_t count) +{ + LUAU_ASSERT(FFlag::LuauLoadUserdataInfo); + + size_t offset = 0; + + uint32_t typeSize = readVarInt(data, size, offset); + uint32_t upvalCount = readVarInt(data, size, offset); + uint32_t localCount = readVarInt(data, size, offset); + + if (typeSize != 0) + { + uint8_t* types = (uint8_t*)data + offset; + + // Skip two bytes of function type introduction + for (uint32_t i = 2; i < typeSize; i++) + { + uint32_t index = uint32_t(types[i] - LBC_TYPE_TAGGED_USERDATA_BASE); + + if (index < count) + types[i] = userdataRemapping[index]; + } + + offset += typeSize; + } + + if (upvalCount != 0) + { + uint8_t* types = (uint8_t*)data + offset; + + for (uint32_t i = 0; i < upvalCount; i++) + { + uint32_t index = uint32_t(types[i] - LBC_TYPE_TAGGED_USERDATA_BASE); + + if (index < count) + types[i] = userdataRemapping[index]; + } + + offset += upvalCount; + } + + if (localCount != 0) + { + for (uint32_t i = 0; i < localCount; i++) + { + uint32_t index = uint32_t(data[offset] - LBC_TYPE_TAGGED_USERDATA_BASE); + + if (index < count) + data[offset] = userdataRemapping[index]; + + offset += 2; + readVarInt(data, size, offset); + readVarInt(data, size, offset); + } + } + + LUAU_ASSERT(offset == size); +} + int luau_load(lua_State* L, const char* chunkname, const char* data, size_t size, int env) { size_t offset = 0; @@ -227,6 +287,18 @@ int luau_load(lua_State* L, const char* chunkname, const char* data, size_t size if (version >= 4) { typesversion = read(data, size, offset); + + if (FFlag::LuauLoadUserdataInfo) + { + if (typesversion < LBC_TYPE_VERSION_MIN || typesversion > LBC_TYPE_VERSION_MAX) + { + char chunkbuf[LUA_IDSIZE]; + const char* chunkid = luaO_chunkid(chunkbuf, sizeof(chunkbuf), chunkname, strlen(chunkname)); + lua_pushfstring(L, "%s: bytecode type version mismatch (expected [%d..%d], got %d)", chunkid, LBC_TYPE_VERSION_MIN, + LBC_TYPE_VERSION_MAX, typesversion); + return 1; + } + } } // string table @@ -241,6 +313,31 @@ int luau_load(lua_State* L, const char* chunkname, const char* data, size_t size offset += length; } + // userdata type remapping table + // for unknown userdata types, the entry will remap to common 'userdata' type + const uint32_t userdataTypeLimit = LBC_TYPE_TAGGED_USERDATA_END - LBC_TYPE_TAGGED_USERDATA_BASE; + uint8_t userdataRemapping[userdataTypeLimit]; + + if (FFlag::LuauLoadUserdataInfo && typesversion == 3) + { + memset(userdataRemapping, LBC_TYPE_USERDATA, userdataTypeLimit); + + uint8_t index = read(data, size, offset); + + while (index != 0) + { + TString* name = readString(strings, data, size, offset); + + if (uint32_t(index - 1) < userdataTypeLimit) + { + if (auto cb = L->global->ecb.gettypemapping) + userdataRemapping[index - 1] = cb(L, getstr(name), name->len); + } + + index = read(data, size, offset); + } + } + // proto table unsigned int protoCount = readVarInt(data, size, offset); TempBuffer protos(L, protoCount); @@ -299,7 +396,7 @@ int luau_load(lua_State* L, const char* chunkname, const char* data, size_t size offset += typesize; } - else if (typesversion == 2) + else if (typesversion == 2 || (FFlag::LuauLoadUserdataInfo && typesversion == 3)) { uint32_t typesize = readVarInt(data, size, offset); @@ -311,6 +408,11 @@ int luau_load(lua_State* L, const char* chunkname, const char* data, size_t size p->sizetypeinfo = typesize; memcpy(p->typeinfo, types, typesize); offset += typesize; + + if (FFlag::LuauLoadUserdataInfo && typesversion == 3) + { + remapUserdataTypes((char*)(uint8_t*)p->typeinfo, p->sizetypeinfo, userdataRemapping, userdataTypeLimit); + } } } } diff --git a/tests/Compiler.test.cpp b/tests/Compiler.test.cpp index e8927837..6255d73f 100644 --- a/tests/Compiler.test.cpp +++ b/tests/Compiler.test.cpp @@ -22,8 +22,9 @@ LUAU_FASTINT(LuauCompileLoopUnrollThreshold) LUAU_FASTINT(LuauCompileLoopUnrollThresholdMaxBoost) LUAU_FASTINT(LuauRecursionLimit) -LUAU_FASTFLAG(LuauCompileNoJumpLineRetarget) -LUAU_FASTFLAG(LuauCompileRepeatUntilSkippedLocals) +LUAU_FASTFLAG(LuauCompileTypeInfo) +LUAU_FASTFLAG(LuauCompileTempTypeInfo) +LUAU_FASTFLAG(LuauCompileUserdataInfo) using namespace Luau; @@ -2106,8 +2107,6 @@ RETURN R0 0 TEST_CASE("LoopContinueEarlyCleanup") { - ScopedFastFlag luauCompileRepeatUntilSkippedLocals{FFlag::LuauCompileRepeatUntilSkippedLocals, true}; - // locals after a potential 'continue' are not accessible inside the condition and can be closed at the end of a block CHECK_EQ("\n" + compileFunction(R"( local y @@ -2788,8 +2787,6 @@ end TEST_CASE("DebugLineInfoWhile") { - ScopedFastFlag luauCompileNoJumpLineRetarget{FFlag::LuauCompileNoJumpLineRetarget, true}; - Luau::BytecodeBuilder bcb; bcb.setDumpFlags(Luau::BytecodeBuilder::Dump_Code | Luau::BytecodeBuilder::Dump_Lines); Luau::compileOrThrow(bcb, R"( @@ -3136,8 +3133,6 @@ local 8: reg 3, start pc 35 line 21, end pc 35 line 21 TEST_CASE("DebugLocals2") { - ScopedFastFlag luauCompileRepeatUntilSkippedLocals{FFlag::LuauCompileRepeatUntilSkippedLocals, true}; - const char* source = R"( function foo(x) repeat @@ -3167,9 +3162,6 @@ local 2: reg 0, start pc 0 line 4, end pc 2 line 6 TEST_CASE("DebugLocals3") { - ScopedFastFlag luauCompileRepeatUntilSkippedLocals{FFlag::LuauCompileRepeatUntilSkippedLocals, true}; - ScopedFastFlag luauCompileNoJumpLineRetarget{FFlag::LuauCompileNoJumpLineRetarget, true}; - const char* source = R"( function foo(x) repeat @@ -3203,6 +3195,7 @@ local 4: reg 0, start pc 0 line 4, end pc 5 line 8 8: RETURN R0 0 )"); } + TEST_CASE("DebugRemarks") { Luau::BytecodeBuilder bcb; @@ -3230,6 +3223,80 @@ RETURN R0 0 )"); } +TEST_CASE("DebugTypes") +{ + ScopedFastFlag luauCompileTypeInfo{FFlag::LuauCompileTypeInfo, true}; + ScopedFastFlag luauCompileTempTypeInfo{FFlag::LuauCompileTempTypeInfo, true}; + ScopedFastFlag luauCompileUserdataInfo{FFlag::LuauCompileUserdataInfo, true}; + + const char* source = R"( +local up: number = 2 + +function foo(e: vector, f: mat3, g: sequence) + local h = e * e + + for i=1,3 do + print(i) + end + + print(e * f) + print(g) + print(h) + + up += a + return a +end +)"; + + Luau::BytecodeBuilder bcb; + bcb.setDumpFlags(Luau::BytecodeBuilder::Dump_Code | Luau::BytecodeBuilder::Dump_Types); + bcb.setDumpSource(source); + + Luau::CompileOptions options; + options.vectorCtor = "vector"; + options.vectorType = "vector"; + + options.typeInfoLevel = 1; + + static const char* kUserdataCompileTypes[] = {"vec2", "color", "mat3", nullptr}; + options.userdataTypes = kUserdataCompileTypes; + + Luau::compileOrThrow(bcb, source, options); + + CHECK_EQ("\n" + bcb.dumpFunction(0), R"( +R0: vector [argument] +R1: mat3 [argument] +R2: userdata [argument] +U0: number +R6: any from 1 to 9 +R3: vector from 0 to 30 +MUL R3 R0 R0 +LOADN R6 1 +LOADN R4 3 +LOADN R5 1 +FORNPREP R4 L1 +L0: GETIMPORT R7 1 [print] +MOVE R8 R6 +CALL R7 1 0 +FORNLOOP R4 L0 +L1: GETIMPORT R4 1 [print] +MUL R5 R0 R1 +CALL R4 1 0 +GETIMPORT R4 1 [print] +MOVE R5 R2 +CALL R4 1 0 +GETIMPORT R4 1 [print] +MOVE R5 R3 +CALL R4 1 0 +GETUPVAL R4 0 +GETIMPORT R5 3 [a] +ADD R4 R4 R5 +SETUPVAL R4 0 +GETIMPORT R4 3 [a] +RETURN R4 1 +)"); +} + TEST_CASE("SourceRemarks") { const char* source = R"( @@ -4158,8 +4225,6 @@ RETURN R0 0 TEST_CASE("Coverage") { - ScopedFastFlag luauCompileNoJumpLineRetarget{FFlag::LuauCompileNoJumpLineRetarget, true}; - // basic statement coverage CHECK_EQ("\n" + compileFunction0Coverage(R"( print(1) diff --git a/tests/Conformance.test.cpp b/tests/Conformance.test.cpp index bd57a140..7ced52cf 100644 --- a/tests/Conformance.test.cpp +++ b/tests/Conformance.test.cpp @@ -33,8 +33,7 @@ void luaC_validate(lua_State* L); LUAU_FASTFLAG(DebugLuauAbortingChecks) LUAU_FASTINT(CodegenHeuristicsInstructionLimit) -LUAU_FASTFLAG(LuauCompileRepeatUntilSkippedLocals) -LUAU_DYNAMIC_FASTFLAG(LuauFastCrossTableMove) +LUAU_FASTFLAG(LuauCodegenFixSplitStoreConstMismatch) static lua_CompileOptions defaultOptions() { @@ -443,8 +442,6 @@ TEST_CASE("Sort") TEST_CASE("Move") { - ScopedFastFlag luauFastCrossTableMove{DFFlag::LuauFastCrossTableMove, true}; - runConformance("move.lua"); } @@ -717,8 +714,6 @@ TEST_CASE("Debugger") static bool singlestep = false; static int stephits = 0; - ScopedFastFlag luauCompileRepeatUntilSkippedLocals{FFlag::LuauCompileRepeatUntilSkippedLocals, true}; - SUBCASE("") { singlestep = false; @@ -2140,6 +2135,8 @@ TEST_CASE("Native") if (!codegen || !luau_codegen_supported()) return; + ScopedFastFlag luauCodegenFixSplitStoreConstMismatch{FFlag::LuauCodegenFixSplitStoreConstMismatch, true}; + SUBCASE("Checked") { FFlag::DebugLuauAbortingChecks.value = true; diff --git a/tests/ConformanceIrHooks.h b/tests/ConformanceIrHooks.h index 135fe9da..d4050863 100644 --- a/tests/ConformanceIrHooks.h +++ b/tests/ConformanceIrHooks.h @@ -3,6 +3,8 @@ #include "Luau/IrBuilder.h" +static const char* kUserdataRunTypes[] = {"extra", "color", "vec2", "mat3", nullptr}; + inline uint8_t vectorAccessBytecodeType(const char* member, size_t memberLength) { using namespace Luau::CodeGen; diff --git a/tests/IrBuilder.test.cpp b/tests/IrBuilder.test.cpp index 4f7725e6..da7cd9b1 100644 --- a/tests/IrBuilder.test.cpp +++ b/tests/IrBuilder.test.cpp @@ -14,7 +14,7 @@ LUAU_FASTFLAG(LuauCodegenRemoveDeadStores5) LUAU_FASTFLAG(DebugLuauAbortingChecks) -LUAU_FASTFLAG(LuauCodegenLoadPropCheckRegLinkInTv) +LUAU_FASTFLAG(LuauCodegenFixSplitStoreConstMismatch) using namespace Luau::CodeGen; @@ -2658,6 +2658,60 @@ bb_0: )"); } +TEST_CASE_FIXTURE(IrBuilderFixture, "DoNotProduceInvalidSplitStore1") +{ + ScopedFastFlag luauCodegenFixSplitStoreConstMismatch{FFlag::LuauCodegenFixSplitStoreConstMismatch, true}; + + IrOp entry = build.block(IrBlockKind::Internal); + + build.beginBlock(entry); + build.inst(IrCmd::STORE_INT, build.vmReg(0), build.constInt(1)); + build.inst(IrCmd::CHECK_TAG, build.vmReg(0), build.constTag(ttable), build.vmExit(1)); + build.inst(IrCmd::STORE_TVALUE, build.vmReg(1), build.inst(IrCmd::LOAD_TVALUE, build.vmReg(0))); + build.inst(IrCmd::RETURN, build.vmReg(1), build.constInt(1)); + + updateUseCounts(build.function); + computeCfgInfo(build.function); + constPropInBlockChains(build, true); + + CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( +bb_0: + STORE_INT R0, 1i + CHECK_TAG R0, ttable, exit(1) + %2 = LOAD_TVALUE R0 + STORE_TVALUE R1, %2 + RETURN R1, 1i + +)"); +} + +TEST_CASE_FIXTURE(IrBuilderFixture, "DoNotProduceInvalidSplitStore2") +{ + ScopedFastFlag luauCodegenFixSplitStoreConstMismatch{FFlag::LuauCodegenFixSplitStoreConstMismatch, true}; + + IrOp entry = build.block(IrBlockKind::Internal); + + build.beginBlock(entry); + build.inst(IrCmd::STORE_INT, build.vmReg(0), build.constInt(1)); + build.inst(IrCmd::CHECK_TAG, build.vmReg(0), build.constTag(tnumber), build.vmExit(1)); + build.inst(IrCmd::STORE_TVALUE, build.vmReg(1), build.inst(IrCmd::LOAD_TVALUE, build.vmReg(0))); + build.inst(IrCmd::RETURN, build.vmReg(1), build.constInt(1)); + + updateUseCounts(build.function); + computeCfgInfo(build.function); + constPropInBlockChains(build, true); + + CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( +bb_0: + STORE_INT R0, 1i + CHECK_TAG R0, tnumber, exit(1) + %2 = LOAD_TVALUE R0 + STORE_TVALUE R1, %2 + RETURN R1, 1i + +)"); +} + TEST_SUITE_END(); TEST_SUITE_BEGIN("Analysis"); @@ -3475,8 +3529,6 @@ bb_1: TEST_CASE_FIXTURE(IrBuilderFixture, "TaggedValuePropagationIntoTvalueChecksRegisterVersion") { - ScopedFastFlag luauCodegenLoadPropCheckRegLinkInTv{FFlag::LuauCodegenLoadPropCheckRegLinkInTv, true}; - IrOp entry = build.block(IrBlockKind::Internal); build.beginBlock(entry); diff --git a/tests/IrLowering.test.cpp b/tests/IrLowering.test.cpp index 131ec4d1..5d7fedd8 100644 --- a/tests/IrLowering.test.cpp +++ b/tests/IrLowering.test.cpp @@ -13,18 +13,17 @@ #include "ConformanceIrHooks.h" #include +#include LUAU_FASTFLAG(LuauCodegenRemoveDeadStores5) LUAU_FASTFLAG(LuauCodegenDirectUserdataFlow) LUAU_FASTFLAG(LuauCompileTypeInfo) LUAU_FASTFLAG(LuauLoadTypeInfo) LUAU_FASTFLAG(LuauCodegenTypeInfo) -LUAU_FASTFLAG(LuauTypeInfoLookupImprovement) -LUAU_FASTFLAG(LuauCodegenIrTypeNames) LUAU_FASTFLAG(LuauCompileTempTypeInfo) -LUAU_FASTFLAG(LuauCodegenFixVectorFields) -LUAU_FASTFLAG(LuauCodegenVectorMispredictFix) LUAU_FASTFLAG(LuauCodegenAnalyzeHostVectorOps) +LUAU_FASTFLAG(LuauCompileUserdataInfo) +LUAU_FASTFLAG(LuauLoadUserdataInfo) static std::string getCodegenAssembly(const char* source, bool includeIrTypes = false, int debugLevel = 1) { @@ -64,6 +63,9 @@ static std::string getCodegenAssembly(const char* source, bool includeIrTypes = copts.vectorCtor = "vector"; copts.vectorType = "vector"; + static const char* kUserdataCompileTypes[] = {"vec2", "color", "mat3", nullptr}; + copts.userdataTypes = kUserdataCompileTypes; + Luau::BytecodeBuilder bcb; Luau::compileOrThrow(bcb, result, names, copts); @@ -71,6 +73,33 @@ static std::string getCodegenAssembly(const char* source, bool includeIrTypes = std::unique_ptr globalState(luaL_newstate(), lua_close); lua_State* L = globalState.get(); + // Runtime mapping is specifically created to NOT match the compilation mapping + options.compilationOptions.userdataTypes = kUserdataRunTypes; + + if (Luau::CodeGen::isSupported()) + { + // Type remapper requires the codegen runtime + Luau::CodeGen::create(L); + + Luau::CodeGen::setUserdataRemapper(L, kUserdataRunTypes, [](void* context, const char* str, size_t len) -> uint8_t { + const char** types = (const char**)context; + + uint8_t index = 0; + + std::string_view sv{str, len}; + + for (; *types; ++types) + { + if (sv == *types) + return index; + + index++; + } + + return 0xff; + }); + } + if (luau_load(L, "name", bytecode.data(), bytecode.size(), 0) == 0) return Luau::CodeGen::getAssembly(L, -1, options, nullptr); @@ -480,8 +509,6 @@ bb_bytecode_1: TEST_CASE("VectorRandomProp") { ScopedFastFlag luauCodegenRemoveDeadStores{FFlag::LuauCodegenRemoveDeadStores5, true}; - ScopedFastFlag luauCodegenFixVectorFields{FFlag::LuauCodegenFixVectorFields, true}; - ScopedFastFlag luauCodegenVectorMispredictFix{FFlag::LuauCodegenVectorMispredictFix, true}; CHECK_EQ("\n" + getCodegenAssembly(R"( local function foo(a: vector) @@ -524,7 +551,6 @@ bb_6: TEST_CASE("VectorCustomAccess") { ScopedFastFlag luauCodegenRemoveDeadStores{FFlag::LuauCodegenRemoveDeadStores5, true}; - ScopedFastFlag luauCodegenVectorMispredictFix{FFlag::LuauCodegenVectorMispredictFix, true}; ScopedFastFlag luauCodegenAnalyzeHostVectorOps{FFlag::LuauCodegenAnalyzeHostVectorOps, true}; CHECK_EQ("\n" + getCodegenAssembly(R"( @@ -600,7 +626,6 @@ bb_bytecode_1: TEST_CASE("VectorCustomAccessChain") { ScopedFastFlag luauCodegenRemoveDeadStores{FFlag::LuauCodegenRemoveDeadStores5, true}; - ScopedFastFlag luauCodegenVectorMispredictFix{FFlag::LuauCodegenVectorMispredictFix, true}; ScopedFastFlag LuauCodegenDirectUserdataFlow{FFlag::LuauCodegenDirectUserdataFlow, true}; ScopedFastFlag luauCodegenAnalyzeHostVectorOps{FFlag::LuauCodegenAnalyzeHostVectorOps, true}; @@ -655,7 +680,6 @@ bb_bytecode_1: TEST_CASE("VectorCustomNamecallChain") { ScopedFastFlag luauCodegenRemoveDeadStores{FFlag::LuauCodegenRemoveDeadStores5, true}; - ScopedFastFlag luauCodegenVectorMispredictFix{FFlag::LuauCodegenVectorMispredictFix, true}; ScopedFastFlag LuauCodegenDirectUserdataFlow{FFlag::LuauCodegenDirectUserdataFlow, true}; ScopedFastFlag luauCodegenAnalyzeHostVectorOps{FFlag::LuauCodegenAnalyzeHostVectorOps, true}; @@ -717,8 +741,8 @@ bb_bytecode_1: TEST_CASE("VectorCustomNamecallChain2") { ScopedFastFlag sffs[]{{FFlag::LuauLoadTypeInfo, true}, {FFlag::LuauCompileTypeInfo, true}, {FFlag::LuauCodegenTypeInfo, true}, - {FFlag::LuauCodegenRemoveDeadStores5, true}, {FFlag::LuauTypeInfoLookupImprovement, true}, {FFlag::LuauCompileTempTypeInfo, true}, - {FFlag::LuauCodegenVectorMispredictFix, true}, {FFlag::LuauCodegenDirectUserdataFlow, true}, {FFlag::LuauCodegenAnalyzeHostVectorOps, true}}; + {FFlag::LuauCodegenRemoveDeadStores5, true}, {FFlag::LuauCompileTempTypeInfo, true}, {FFlag::LuauCodegenDirectUserdataFlow, true}, + {FFlag::LuauCodegenAnalyzeHostVectorOps, true}}; CHECK_EQ("\n" + getCodegenAssembly(R"( type Vertex = {n: vector, b: vector} @@ -1048,7 +1072,7 @@ bb_bytecode_1: TEST_CASE("LoadAndMoveTypePropagation") { ScopedFastFlag sffs[]{{FFlag::LuauLoadTypeInfo, true}, {FFlag::LuauCompileTypeInfo, true}, {FFlag::LuauCodegenTypeInfo, true}, - {FFlag::LuauCodegenRemoveDeadStores5, true}, {FFlag::LuauTypeInfoLookupImprovement, true}}; + {FFlag::LuauCodegenRemoveDeadStores5, true}}; CHECK_EQ("\n" + getCodegenAssembly(R"( local function getsum(n) @@ -1116,7 +1140,7 @@ bb_bytecode_4: TEST_CASE("ArgumentTypeRefinement") { ScopedFastFlag sffs[]{{FFlag::LuauLoadTypeInfo, true}, {FFlag::LuauCompileTypeInfo, true}, {FFlag::LuauCodegenTypeInfo, true}, - {FFlag::LuauCodegenRemoveDeadStores5, true}, {FFlag::LuauTypeInfoLookupImprovement, true}}; + {FFlag::LuauCodegenRemoveDeadStores5, true}}; CHECK_EQ("\n" + getCodegenAssembly(R"( local function getsum(x, y) @@ -1155,7 +1179,7 @@ bb_bytecode_0: TEST_CASE("InlineFunctionType") { ScopedFastFlag sffs[]{{FFlag::LuauLoadTypeInfo, true}, {FFlag::LuauCompileTypeInfo, true}, {FFlag::LuauCodegenTypeInfo, true}, - {FFlag::LuauCodegenRemoveDeadStores5, true}, {FFlag::LuauTypeInfoLookupImprovement, true}}; + {FFlag::LuauCodegenRemoveDeadStores5, true}}; CHECK_EQ("\n" + getCodegenAssembly(R"( local function inl(v: vector, s: number) @@ -1204,8 +1228,7 @@ bb_bytecode_0: TEST_CASE("ResolveTablePathTypes") { ScopedFastFlag sffs[]{{FFlag::LuauLoadTypeInfo, true}, {FFlag::LuauCompileTypeInfo, true}, {FFlag::LuauCodegenTypeInfo, true}, - {FFlag::LuauCodegenRemoveDeadStores5, true}, {FFlag::LuauTypeInfoLookupImprovement, true}, {FFlag::LuauCodegenIrTypeNames, true}, - {FFlag::LuauCompileTempTypeInfo, true}}; + {FFlag::LuauCodegenRemoveDeadStores5, true}, {FFlag::LuauCompileTempTypeInfo, true}}; CHECK_EQ("\n" + getCodegenAssembly(R"( type Vertex = {pos: vector, normal: vector} @@ -1260,8 +1283,7 @@ bb_6: TEST_CASE("ResolvableSimpleMath") { ScopedFastFlag sffs[]{{FFlag::LuauLoadTypeInfo, true}, {FFlag::LuauCompileTypeInfo, true}, {FFlag::LuauCodegenTypeInfo, true}, - {FFlag::LuauCodegenRemoveDeadStores5, true}, {FFlag::LuauTypeInfoLookupImprovement, true}, {FFlag::LuauCodegenIrTypeNames, true}, - {FFlag::LuauCompileTempTypeInfo, true}}; + {FFlag::LuauCodegenRemoveDeadStores5, true}, {FFlag::LuauCompileTempTypeInfo, true}}; CHECK_EQ("\n" + getCodegenHeader(R"( type Vertex = { p: vector, uv: vector, n: vector, t: vector, b: vector, h: number } @@ -1318,8 +1340,8 @@ end TEST_CASE("ResolveVectorNamecalls") { ScopedFastFlag sffs[]{{FFlag::LuauLoadTypeInfo, true}, {FFlag::LuauCompileTypeInfo, true}, {FFlag::LuauCodegenTypeInfo, true}, - {FFlag::LuauCodegenRemoveDeadStores5, true}, {FFlag::LuauTypeInfoLookupImprovement, true}, {FFlag::LuauCodegenIrTypeNames, true}, - {FFlag::LuauCompileTempTypeInfo, true}, {FFlag::LuauCodegenDirectUserdataFlow, true}, {FFlag::LuauCodegenAnalyzeHostVectorOps, true}}; + {FFlag::LuauCodegenRemoveDeadStores5, true}, {FFlag::LuauCompileTempTypeInfo, true}, {FFlag::LuauCodegenDirectUserdataFlow, true}, + {FFlag::LuauCodegenAnalyzeHostVectorOps, true}}; CHECK_EQ("\n" + getCodegenAssembly(R"( type Vertex = {pos: vector, normal: vector} @@ -1384,8 +1406,7 @@ bb_6: TEST_CASE("ImmediateTypeAnnotationHelp") { ScopedFastFlag sffs[]{{FFlag::LuauLoadTypeInfo, true}, {FFlag::LuauCompileTypeInfo, true}, {FFlag::LuauCodegenTypeInfo, true}, - {FFlag::LuauCodegenRemoveDeadStores5, true}, {FFlag::LuauTypeInfoLookupImprovement, true}, {FFlag::LuauCodegenIrTypeNames, true}, - {FFlag::LuauCompileTempTypeInfo, true}}; + {FFlag::LuauCodegenRemoveDeadStores5, true}, {FFlag::LuauCompileTempTypeInfo, true}}; CHECK_EQ("\n" + getCodegenAssembly(R"( local function foo(arr, i) @@ -1424,8 +1445,7 @@ bb_2: TEST_CASE("UnaryTypeResolve") { ScopedFastFlag sffs[]{{FFlag::LuauLoadTypeInfo, true}, {FFlag::LuauCompileTypeInfo, true}, {FFlag::LuauCodegenTypeInfo, true}, - {FFlag::LuauCodegenRemoveDeadStores5, true}, {FFlag::LuauTypeInfoLookupImprovement, true}, {FFlag::LuauCodegenIrTypeNames, true}, - {FFlag::LuauCompileTempTypeInfo, true}}; + {FFlag::LuauCodegenRemoveDeadStores5, true}, {FFlag::LuauCompileTempTypeInfo, true}}; CHECK_EQ("\n" + getCodegenHeader(R"( local function foo(a, b: vector, c) @@ -1448,8 +1468,7 @@ end TEST_CASE("ForInManualAnnotation") { ScopedFastFlag sffs[]{{FFlag::LuauLoadTypeInfo, true}, {FFlag::LuauCompileTypeInfo, true}, {FFlag::LuauCodegenTypeInfo, true}, - {FFlag::LuauCodegenRemoveDeadStores5, true}, {FFlag::LuauTypeInfoLookupImprovement, true}, {FFlag::LuauCodegenIrTypeNames, true}, - {FFlag::LuauCompileTempTypeInfo, true}}; + {FFlag::LuauCodegenRemoveDeadStores5, true}, {FFlag::LuauCompileTempTypeInfo, true}}; CHECK_EQ("\n" + getCodegenAssembly(R"( type Vertex = {pos: vector, normal: vector} @@ -1545,8 +1564,7 @@ bb_12: TEST_CASE("ForInAutoAnnotationIpairs") { ScopedFastFlag sffs[]{{FFlag::LuauLoadTypeInfo, true}, {FFlag::LuauCompileTypeInfo, true}, {FFlag::LuauCodegenTypeInfo, true}, - {FFlag::LuauCodegenRemoveDeadStores5, true}, {FFlag::LuauTypeInfoLookupImprovement, true}, {FFlag::LuauCodegenIrTypeNames, true}, - {FFlag::LuauCompileTempTypeInfo, true}}; + {FFlag::LuauCodegenRemoveDeadStores5, true}, {FFlag::LuauCompileTempTypeInfo, true}}; CHECK_EQ("\n" + getCodegenHeader(R"( type Vertex = {pos: vector, normal: vector} @@ -1574,8 +1592,7 @@ end TEST_CASE("ForInAutoAnnotationPairs") { ScopedFastFlag sffs[]{{FFlag::LuauLoadTypeInfo, true}, {FFlag::LuauCompileTypeInfo, true}, {FFlag::LuauCodegenTypeInfo, true}, - {FFlag::LuauCodegenRemoveDeadStores5, true}, {FFlag::LuauTypeInfoLookupImprovement, true}, {FFlag::LuauCodegenIrTypeNames, true}, - {FFlag::LuauCompileTempTypeInfo, true}}; + {FFlag::LuauCodegenRemoveDeadStores5, true}, {FFlag::LuauCompileTempTypeInfo, true}}; CHECK_EQ("\n" + getCodegenHeader(R"( type Vertex = {pos: vector, normal: vector} @@ -1603,8 +1620,7 @@ end TEST_CASE("ForInAutoAnnotationGeneric") { ScopedFastFlag sffs[]{{FFlag::LuauLoadTypeInfo, true}, {FFlag::LuauCompileTypeInfo, true}, {FFlag::LuauCodegenTypeInfo, true}, - {FFlag::LuauCodegenRemoveDeadStores5, true}, {FFlag::LuauTypeInfoLookupImprovement, true}, {FFlag::LuauCodegenIrTypeNames, true}, - {FFlag::LuauCompileTempTypeInfo, true}}; + {FFlag::LuauCodegenRemoveDeadStores5, true}, {FFlag::LuauCompileTempTypeInfo, true}}; CHECK_EQ("\n" + getCodegenHeader(R"( type Vertex = {pos: vector, normal: vector} @@ -1629,4 +1645,49 @@ end )"); } +// Temporary test, when we don't compile new typeinfo, but support loading it +TEST_CASE("CustomUserdataTypesTemp") +{ + // This test requires runtime component to be present + if (!Luau::CodeGen::isSupported()) + return; + + ScopedFastFlag sffs[]{{FFlag::LuauLoadTypeInfo, true}, {FFlag::LuauCompileTypeInfo, true}, {FFlag::LuauCodegenTypeInfo, true}, + {FFlag::LuauCodegenRemoveDeadStores5, true}, {FFlag::LuauCompileTempTypeInfo, true}, {FFlag::LuauCompileUserdataInfo, false}, + {FFlag::LuauLoadUserdataInfo, true}}; + + CHECK_EQ("\n" + getCodegenHeader(R"( +local function foo(v: vec2, x: mat3) + return v.X * x +end +)"), + R"( +; function foo(v, x) line 2 +; R0: userdata [argument 'v'] +; R1: userdata [argument 'x'] +)"); +} + +TEST_CASE("CustomUserdataTypes") +{ + // This test requires runtime component to be present + if (!Luau::CodeGen::isSupported()) + return; + + ScopedFastFlag sffs[]{{FFlag::LuauLoadTypeInfo, true}, {FFlag::LuauCompileTypeInfo, true}, {FFlag::LuauCodegenTypeInfo, true}, + {FFlag::LuauCodegenRemoveDeadStores5, true}, {FFlag::LuauCompileTempTypeInfo, true}, {FFlag::LuauCompileUserdataInfo, true}, + {FFlag::LuauLoadUserdataInfo, true}}; + + CHECK_EQ("\n" + getCodegenHeader(R"( +local function foo(v: vec2, x: mat3) + return v.X * x +end +)"), + R"( +; function foo(v, x) line 2 +; R0: vec2 [argument 'v'] +; R1: mat3 [argument 'x'] +)"); +} + TEST_SUITE_END(); diff --git a/tests/NonStrictTypeChecker.test.cpp b/tests/NonStrictTypeChecker.test.cpp index 806dac62..e51fb0df 100644 --- a/tests/NonStrictTypeChecker.test.cpp +++ b/tests/NonStrictTypeChecker.test.cpp @@ -556,4 +556,22 @@ local E = require(script.Parent.A) LUAU_REQUIRE_NO_ERRORS(result); } +TEST_CASE_FIXTURE(NonStrictTypeCheckerFixture, "nonstrict_shouldnt_warn_on_valid_buffer_use") +{ + loadDefinition(R"( +declare buffer: { + create: @checked (size: number) -> buffer, + readi8: @checked (b: buffer, offset: number) -> number, + writef64: @checked (b: buffer, offset: number, value: number) -> (), +} +)"); + + CheckResult result = checkNonStrict(R"( +local b = buffer.create(100) +buffer.writef64(b, 0, 5) +buffer.readi8(b, 0) +)"); + LUAU_REQUIRE_NO_ERRORS(result); +} + TEST_SUITE_END(); diff --git a/tests/Repl.test.cpp b/tests/Repl.test.cpp index c22d464e..3eceea17 100644 --- a/tests/Repl.test.cpp +++ b/tests/Repl.test.cpp @@ -420,4 +420,22 @@ print(NewProxyOne.HelloICauseACrash) )"); } +TEST_CASE_FIXTURE(ReplFixture, "InteractiveStackReserve1") +{ + // Reset stack reservation + lua_resume(L, nullptr, 0); + + runCode(L, R"( +local t = {} +)"); +} + +TEST_CASE_FIXTURE(ReplFixture, "InteractiveStackReserve2") +{ + // Reset stack reservation + lua_resume(L, nullptr, 0); + + getCompletionSet("a"); +} + TEST_SUITE_END(); diff --git a/tests/Subtyping.test.cpp b/tests/Subtyping.test.cpp index d8f115ae..afc22d0f 100644 --- a/tests/Subtyping.test.cpp +++ b/tests/Subtyping.test.cpp @@ -915,6 +915,7 @@ TEST_IS_SUBTYPE(numberToNumberType, negate(builtinTypes->classType)); TEST_IS_NOT_SUBTYPE(numberToNumberType, negate(builtinTypes->functionType)); // Negated supertypes: Primitives and singletons +TEST_IS_NOT_SUBTYPE(builtinTypes->stringType, negate(builtinTypes->stringType)); TEST_IS_SUBTYPE(builtinTypes->stringType, negate(builtinTypes->numberType)); TEST_IS_SUBTYPE(str("foo"), meet(builtinTypes->stringType, negate(str("bar")))); TEST_IS_NOT_SUBTYPE(builtinTypes->trueType, negate(builtinTypes->booleanType)); diff --git a/tests/ToString.test.cpp b/tests/ToString.test.cpp index b2c5f623..e2b3f9b7 100644 --- a/tests/ToString.test.cpp +++ b/tests/ToString.test.cpp @@ -782,7 +782,10 @@ TEST_CASE_FIXTURE(Fixture, "toStringNamedFunction_map") TypeId ty = requireType("map"); const FunctionType* ftv = get(follow(ty)); - CHECK_EQ("map(arr: {a}, fn: (a) -> b): {b}", toStringNamedFunction("map", *ftv)); + if (FFlag::DebugLuauDeferredConstraintResolution) + CHECK_EQ("map(arr: {a}, fn: (a) -> (b, ...unknown)): {b}", toStringNamedFunction("map", *ftv)); + else + CHECK_EQ("map(arr: {a}, fn: (a) -> b): {b}", toStringNamedFunction("map", *ftv)); } TEST_CASE_FIXTURE(Fixture, "toStringNamedFunction_generic_pack") diff --git a/tests/TypeFamily.test.cpp b/tests/TypeFamily.test.cpp index 88dfbf47..c66f0227 100644 --- a/tests/TypeFamily.test.cpp +++ b/tests/TypeFamily.test.cpp @@ -3,7 +3,6 @@ #include "Luau/ConstraintSolver.h" #include "Luau/NotNull.h" -#include "Luau/TxnLog.h" #include "Luau/Type.h" #include "ClassFixture.h" @@ -14,6 +13,7 @@ using namespace Luau; LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution) +LUAU_DYNAMIC_FASTINT(LuauTypeFamilyApplicationCartesianProductLimit) struct FamilyFixture : Fixture { @@ -24,7 +24,7 @@ struct FamilyFixture : Fixture { swapFamily = TypeFamily{/* name */ "Swap", /* reducer */ - [](TypeId instance, NotNull queue, const std::vector& tys, const std::vector& tps, + [](TypeId instance, const std::vector& tys, const std::vector& tps, NotNull ctx) -> TypeFamilyReductionResult { LUAU_ASSERT(tys.size() == 1); TypeId param = follow(tys.at(0)); @@ -716,4 +716,117 @@ _(setmetatable(_,{[...]=_,})) )"); } +TEST_CASE_FIXTURE(BuiltinsFixture, "cyclic_concat_family_at_work") +{ + if (!FFlag::DebugLuauDeferredConstraintResolution) + return; + + CheckResult result = check(R"( + type T = concat + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + CHECK(toString(requireTypeAlias("T")) == "string"); +} + +TEST_CASE_FIXTURE(BuiltinsFixture, "exceeded_distributivity_limits") +{ + if (!FFlag::DebugLuauDeferredConstraintResolution) + return; + + ScopedFastInt sfi{DFInt::LuauTypeFamilyApplicationCartesianProductLimit, 10}; + + loadDefinition(R"( + declare class A + function __mul(self, rhs: unknown): A + end + + declare class B + function __mul(self, rhs: unknown): B + end + + declare class C + function __mul(self, rhs: unknown): C + end + + declare class D + function __mul(self, rhs: unknown): D + end + )"); + + CheckResult result = check(R"( + type T = mul + )"); + + LUAU_REQUIRE_ERROR_COUNT(1, result); + CHECK(get(result.errors[0])); +} + +TEST_CASE_FIXTURE(BuiltinsFixture, "didnt_quite_exceed_distributivity_limits") +{ + if (!FFlag::DebugLuauDeferredConstraintResolution) + return; + + // We duplicate the test here because we want to make sure the test failed + // due to exceeding the limits specifically, rather than any possible reasons. + ScopedFastInt sfi{DFInt::LuauTypeFamilyApplicationCartesianProductLimit, 20}; + + loadDefinition(R"( + declare class A + function __mul(self, rhs: unknown): A + end + + declare class B + function __mul(self, rhs: unknown): B + end + + declare class C + function __mul(self, rhs: unknown): C + end + + declare class D + function __mul(self, rhs: unknown): D + end + )"); + + CheckResult result = check(R"( + type T = mul + )"); + + LUAU_REQUIRE_NO_ERRORS(result); +} + +TEST_CASE_FIXTURE(BuiltinsFixture, "ensure_equivalence_with_distributivity") +{ + if (!FFlag::DebugLuauDeferredConstraintResolution) + return; + + loadDefinition(R"( + declare class A + function __mul(self, rhs: unknown): A + end + + declare class B + function __mul(self, rhs: unknown): B + end + + declare class C + function __mul(self, rhs: unknown): C + end + + declare class D + function __mul(self, rhs: unknown): D + end + )"); + + CheckResult result = check(R"( + type T = mul + type U = mul | mul | mul | mul + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + CHECK(toString(requireTypeAlias("T")) == "A | B"); + CHECK(toString(requireTypeAlias("U")) == "A | A | B | B"); +} + TEST_SUITE_END(); diff --git a/tests/TypeInfer.anyerror.test.cpp b/tests/TypeInfer.anyerror.test.cpp index 8d14f56b..b305d97d 100644 --- a/tests/TypeInfer.anyerror.test.cpp +++ b/tests/TypeInfer.anyerror.test.cpp @@ -32,15 +32,7 @@ TEST_CASE_FIXTURE(Fixture, "for_in_loop_iterator_returns_any") LUAU_REQUIRE_NO_ERRORS(result); - if (FFlag::DebugLuauDeferredConstraintResolution) - { - // Bug: We do not simplify at the right time - CHECK_EQ("any?", toString(requireType("a"))); - } - else - { - CHECK_EQ(builtinTypes->anyType, requireType("a")); - } + CHECK(builtinTypes->anyType == requireType("a")); } TEST_CASE_FIXTURE(Fixture, "for_in_loop_iterator_returns_any2") @@ -58,15 +50,7 @@ TEST_CASE_FIXTURE(Fixture, "for_in_loop_iterator_returns_any2") LUAU_REQUIRE_NO_ERRORS(result); - if (FFlag::DebugLuauDeferredConstraintResolution) - { - // Bug: We do not simplify at the right time - CHECK_EQ("any?", toString(requireType("a"))); - } - else - { - CHECK_EQ("any", toString(requireType("a"))); - } + CHECK("any" == toString(requireType("a"))); } TEST_CASE_FIXTURE(Fixture, "for_in_loop_iterator_is_any") @@ -82,15 +66,7 @@ TEST_CASE_FIXTURE(Fixture, "for_in_loop_iterator_is_any") LUAU_REQUIRE_NO_ERRORS(result); - if (FFlag::DebugLuauDeferredConstraintResolution) - { - // Bug: We do not simplify at the right time - CHECK_EQ("any?", toString(requireType("a"))); - } - else - { - CHECK_EQ("any", toString(requireType("a"))); - } + CHECK("any" == toString(requireType("a"))); } TEST_CASE_FIXTURE(Fixture, "for_in_loop_iterator_is_any2") @@ -104,17 +80,7 @@ TEST_CASE_FIXTURE(Fixture, "for_in_loop_iterator_is_any2") end )"); - LUAU_REQUIRE_NO_ERRORS(result); - - if (FFlag::DebugLuauDeferredConstraintResolution) - { - // Bug: We do not simplify at the right time - CHECK_EQ("any?", toString(requireType("a"))); - } - else - { - CHECK_EQ("any", toString(requireType("a"))); - } + CHECK("any" == toString(requireType("a"))); } TEST_CASE_FIXTURE(Fixture, "for_in_loop_iterator_is_any_pack") @@ -130,15 +96,7 @@ TEST_CASE_FIXTURE(Fixture, "for_in_loop_iterator_is_any_pack") LUAU_REQUIRE_NO_ERRORS(result); - if (FFlag::DebugLuauDeferredConstraintResolution) - { - // Bug: We do not simplify at the right time - CHECK_EQ("any?", toString(requireType("a"))); - } - else - { - CHECK_EQ("any", toString(requireType("a"))); - } + CHECK("any" == toString(requireType("a"))); } TEST_CASE_FIXTURE(Fixture, "for_in_loop_iterator_is_error") diff --git a/tests/TypeInfer.functions.test.cpp b/tests/TypeInfer.functions.test.cpp index 34178fd9..48d130dd 100644 --- a/tests/TypeInfer.functions.test.cpp +++ b/tests/TypeInfer.functions.test.cpp @@ -1582,7 +1582,7 @@ TEST_CASE_FIXTURE(Fixture, "inferred_higher_order_functions_are_quantified_at_th if (!result.errors.empty()) { for (const auto& e : result.errors) - printf("%s %s: %s\n", e.moduleName.c_str(), toString(e.location).c_str(), toString(e).c_str()); + MESSAGE(e.moduleName << " " << toString(e.location) << ": " << toString(e)); } } diff --git a/tests/TypeInfer.loops.test.cpp b/tests/TypeInfer.loops.test.cpp index 8bbd3f92..1a7ef973 100644 --- a/tests/TypeInfer.loops.test.cpp +++ b/tests/TypeInfer.loops.test.cpp @@ -433,9 +433,53 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "varlist_declared_by_for_in_loop_should_be_fr end )"); + if (FFlag::DebugLuauDeferredConstraintResolution) + { + LUAU_REQUIRE_ERROR_COUNT(1, result); + auto err = get(result.errors[0]); + CHECK(err != nullptr); + } + else + { + LUAU_REQUIRE_NO_ERRORS(result); + } +} + +TEST_CASE_FIXTURE(BuiltinsFixture, "iter_constraint_before_loop_body") +{ + CheckResult result = check(R"( + local T = { + fields = {}, + } + + function f() + for u, v in pairs(T.fields) do + T.fields[u] = nil + end + end + )"); + LUAU_REQUIRE_NO_ERRORS(result); } +TEST_CASE_FIXTURE(BuiltinsFixture, "rbxl_place_file_crash_for_wrong_constraints") +{ + CheckResult result = check(R"( +local VehicleParameters = { + -- These are default values in the case the package structure is broken + StrutSpringStiffnessFront = 28000, +} + +local function updateFromConfiguration() + for property, value in pairs(VehicleParameters) do + VehicleParameters[property] = value + end +end +)"); + LUAU_REQUIRE_NO_ERRORS(result); +} + + TEST_CASE_FIXTURE(BuiltinsFixture, "properly_infer_iteratee_is_a_free_table") { // In this case, we cannot know the element type of the table {}. It could be anything. diff --git a/tests/TypeInfer.operators.test.cpp b/tests/TypeInfer.operators.test.cpp index 56548608..fac86150 100644 --- a/tests/TypeInfer.operators.test.cpp +++ b/tests/TypeInfer.operators.test.cpp @@ -732,7 +732,11 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "and_binexps_dont_unify") end )"); - LUAU_REQUIRE_NO_ERRORS(result); + // This infers a type for `t` of `{unknown}`, and so it makes sense that `t[1].test` would error. + if (FFlag::DebugLuauDeferredConstraintResolution) + LUAU_REQUIRE_ERROR_COUNT(1, result); + else + LUAU_REQUIRE_NO_ERRORS(result); } TEST_CASE_FIXTURE(Fixture, "error_on_invalid_operand_types_to_relational_operators") diff --git a/tests/TypeInfer.primitives.test.cpp b/tests/TypeInfer.primitives.test.cpp index 640e693b..37f891cb 100644 --- a/tests/TypeInfer.primitives.test.cpp +++ b/tests/TypeInfer.primitives.test.cpp @@ -101,4 +101,14 @@ TEST_CASE("singleton_types") CHECK(result.errors.empty()); } +TEST_CASE_FIXTURE(BuiltinsFixture, "property_of_buffers") +{ + CheckResult result = check(R"( + local b = buffer.create(100) + print(b.foo) + )"); + + LUAU_REQUIRE_ERROR_COUNT(1, result); +} + TEST_SUITE_END(); diff --git a/tests/TypeInfer.singletons.test.cpp b/tests/TypeInfer.singletons.test.cpp index 485a18c6..ebf1fde4 100644 --- a/tests/TypeInfer.singletons.test.cpp +++ b/tests/TypeInfer.singletons.test.cpp @@ -44,6 +44,20 @@ TEST_CASE_FIXTURE(Fixture, "string_singletons") LUAU_REQUIRE_NO_ERRORS(result); } +TEST_CASE_FIXTURE(Fixture, "string_singleton_function_call") +{ + if (!FFlag::DebugLuauDeferredConstraintResolution) + return; + + CheckResult result = check(R"( + local x = "a" + function f(x: "a") end + f(x) + )"); + + LUAU_REQUIRE_NO_ERRORS(result); +} + TEST_CASE_FIXTURE(Fixture, "bool_singletons_mismatch") { CheckResult result = check(R"( diff --git a/tests/TypeInfer.tables.test.cpp b/tests/TypeInfer.tables.test.cpp index bd0a4144..2c6136a4 100644 --- a/tests/TypeInfer.tables.test.cpp +++ b/tests/TypeInfer.tables.test.cpp @@ -2462,10 +2462,7 @@ local x: {number} | number | string local y = #x )"); - if (FFlag::DebugLuauDeferredConstraintResolution) - LUAU_REQUIRE_ERROR_COUNT(2, result); - else - LUAU_REQUIRE_ERROR_COUNT(1, result); + LUAU_REQUIRE_ERROR_COUNT(1, result); } TEST_CASE_FIXTURE(BuiltinsFixture, "dont_hang_when_trying_to_look_up_in_cyclic_metatable_index") @@ -2973,7 +2970,7 @@ c = b const TableType* ttv = get(*ty); REQUIRE(ttv); - CHECK(ttv->instantiatedTypeParams.empty()); + CHECK(0 == ttv->instantiatedTypeParams.size()); } TEST_CASE_FIXTURE(Fixture, "table_indexing_error_location") @@ -4355,19 +4352,6 @@ TEST_CASE_FIXTURE(Fixture, "mymovie_read_write_tables_bug_2") LUAU_REQUIRE_ERRORS(result); } -TEST_CASE_FIXTURE(Fixture, "setindexer_always_transmute") -{ - ScopedFastFlag sff{FFlag::DebugLuauDeferredConstraintResolution, true}; - - CheckResult result = check(R"( - function f(x) - (5)[5] = x - end - )"); - - CHECK_EQ("(*error-type*) -> ()", toString(requireType("f"))); -} - TEST_CASE_FIXTURE(BuiltinsFixture, "instantiated_metatable_frozen_table_clone_mutation") { ScopedFastFlag luauMetatableInstantiationCloneCheck{FFlag::LuauMetatableInstantiationCloneCheck, true}; @@ -4412,6 +4396,21 @@ TEST_CASE_FIXTURE(Fixture, "setprop_on_a_mutating_local_in_both_loops_and_functi LUAU_REQUIRE_ERRORS(result); } +TEST_CASE_FIXTURE(Fixture, "cant_index_this") +{ + CheckResult result = check(R"( + local a: number = 9 + a[18] = "tomfoolery" + )"); + + LUAU_REQUIRE_ERROR_COUNT(1, result); + + NotATable* notATable = get(result.errors[0]); + REQUIRE(notATable); + + CHECK("number" == toString(notATable->ty)); +} + TEST_CASE_FIXTURE(Fixture, "setindexer_multiple_tables_intersection") { ScopedFastFlag sff{FFlag::DebugLuauDeferredConstraintResolution, true}; @@ -4423,8 +4422,8 @@ TEST_CASE_FIXTURE(Fixture, "setindexer_multiple_tables_intersection") end )"); - LUAU_REQUIRE_NO_ERRORS(result); - CHECK("({ [string]: number } & { [thread]: boolean }, boolean | number) -> ()" == toString(requireType("f"))); + LUAU_REQUIRE_ERROR_COUNT(2, result); + CHECK("({ [string]: number } & { [thread]: boolean }, never) -> ()" == toString(requireType("f"))); } TEST_CASE_FIXTURE(Fixture, "insert_a_and_f_of_a_into_table_res_in_a_loop") diff --git a/tests/TypeInfer.typestates.test.cpp b/tests/TypeInfer.typestates.test.cpp index 3116022b..19117447 100644 --- a/tests/TypeInfer.typestates.test.cpp +++ b/tests/TypeInfer.typestates.test.cpp @@ -406,6 +406,7 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "prototyped_recursive_functions_but_has_futur )"); LUAU_REQUIRE_ERROR_COUNT(1, result); + CHECK("((() -> ()) | number)?" == toString(requireType("f"))); } diff --git a/tests/TypeInfer.unionTypes.test.cpp b/tests/TypeInfer.unionTypes.test.cpp index 5f4d2a0e..539b8592 100644 --- a/tests/TypeInfer.unionTypes.test.cpp +++ b/tests/TypeInfer.unionTypes.test.cpp @@ -606,13 +606,7 @@ TEST_CASE_FIXTURE(Fixture, "indexing_into_a_cyclic_union_doesnt_crash") end )"); - // The old solver has a bug: It doesn't consider this goofy thing to be a - // table. It's not really important. What's important is that we don't - // crash, hang, or ICE. - if (FFlag::DebugLuauDeferredConstraintResolution) - LUAU_REQUIRE_NO_ERRORS(result); - else - LUAU_REQUIRE_ERROR_COUNT(1, result); + LUAU_REQUIRE_ERROR_COUNT(1, result); } TEST_CASE_FIXTURE(BuiltinsFixture, "table_union_write_indirect") diff --git a/tests/conformance/move.lua b/tests/conformance/move.lua index 9518219f..bb613157 100644 --- a/tests/conformance/move.lua +++ b/tests/conformance/move.lua @@ -65,30 +65,6 @@ do a = table.move({[minI] = 100}, minI, minI, maxI) eqT(a, {[minI] = 100, [maxI] = 100}) - -- moving small amount of elements (array/hash) using a wide range - a = {} - table.move({1, 2, 3, 4, 5}, -100000000, 100000000, -100000000, a) - eqT(a, {1, 2, 3, 4, 5}) - - a = {} - table.move({1, 2}, -100000000, 100000000, 0, a) - eqT(a, {[100000001] = 1, [100000002] = 2}) - - -- hash part copy - a = {} - table.move({[-1000000] = 1, [-100] = 2, [100] = 3, [100000] = 4}, -100000000, 100000000, 0, a) - eqT(a, {[99000000] = 1, [99999900] = 2, [100000100] = 3, [100100000] = 4}) - - -- precise hash part bounds - a = {} - table.move({[-100000000 - 1] = -1, [-100000000] = 1, [-100] = 2, [100] = 3, [100000000] = 4, [100000000 + 1] = -1}, -100000000, 100000000, 0, a) - eqT(a, {[0] = 1, [99999900] = 2, [100000100] = 3, [200000000] = 4}) - - -- no integer undeflow in corner hash part case - a = {} - table.move({[minI] = 100, [-100] = 2}, minI, minI + 100000000, minI, a) - eqT(a, {[minI] = 100}) - -- hash part skips array slice a = {} table.move({[-1] = 1, [0] = 2, [1] = 3, [2] = 4}, -1, 3, 1, a) @@ -97,6 +73,19 @@ do a = {} table.move({[-1] = 1, [0] = 2, [1] = 3, [2] = 4, [10] = 5, [100] = 6, [1000] = 7}, -1, 3, 1, a) eqT(a, {[1] = 1, [2] = 2, [3] = 3, [4] = 4}) + + -- moving ranges containing nil values into tables with values + a = {1, 2, 3, 4, 5} + table.move({10}, 1, 3, 2, a) + eqT(a, {1, 10, nil, nil, 5}) + + a = {1, 2, 3, 4, 5} + table.move({10}, -1, 1, 2, a) + eqT(a, {1, nil, nil, 10, 5}) + + a = {[-1000] = 1, [1000] = 2, [1] = 3} + table.move({10}, -1000, 1000, -1000, a) + eqT(a, {10}) end checkerror("too many", table.move, {}, 0, maxI, 1) diff --git a/tests/conformance/native.lua b/tests/conformance/native.lua index 094e6b83..03845013 100644 --- a/tests/conformance/native.lua +++ b/tests/conformance/native.lua @@ -208,6 +208,35 @@ end assert(pcall(fuzzfail21) == false) +local function fuzzfail22(...) + local _ = {false,},true,...,l0 + while _ do + _ = true,{unpack(0,_),},l0 + _.n126 = nil + _ = {not _,_=not _,n0=_,_,n0=not _,},_ < _ + return _ > _ + end + return `""` +end + +assert(pcall(fuzzfail22) == false) + +local function fuzzfail23(...) + local _ = {false,},_,...,l0 + while _ do + _ = true,{unpack(_),},l0 + _ = {{[_]=nil,_=not _,_,true,_=nil,},not _,not _,_,bxor=- _,} + do end + break + end + do end + local _ = _,true + do end + local _ = _,true +end + +assert(pcall(fuzzfail23) == false) + local function arraySizeInv1() local t = {1, 2, nil, nil, nil, nil, nil, nil, nil, true} diff --git a/tools/faillist.txt b/tools/faillist.txt index 6939df54..7a214a32 100644 --- a/tools/faillist.txt +++ b/tools/faillist.txt @@ -71,7 +71,6 @@ GenericsTests.no_stack_overflow_from_quantifying GenericsTests.properties_can_be_instantiated_polytypes GenericsTests.quantify_functions_even_if_they_have_an_explicit_generic GenericsTests.self_recursive_instantiated_param -IntersectionTypes.CLI-44817 IntersectionTypes.error_detailed_intersection_all IntersectionTypes.error_detailed_intersection_part IntersectionTypes.intersect_bool_and_false @@ -134,11 +133,8 @@ RefinementTest.call_an_incompatible_function_after_using_typeguard RefinementTest.dataflow_analysis_can_tell_refinements_when_its_appropriate_to_refine_into_nil_or_never RefinementTest.discriminate_from_isa_of_x RefinementTest.discriminate_from_truthiness_of_x -RefinementTest.free_type_is_equal_to_an_lvalue RefinementTest.globals_can_be_narrowed_too RefinementTest.isa_type_refinement_must_be_known_ahead_of_time -RefinementTest.luau_polyfill_isindexkey_refine_conjunction -RefinementTest.luau_polyfill_isindexkey_refine_conjunction_variant RefinementTest.not_t_or_some_prop_of_t RefinementTest.refine_a_param_that_got_resolved_during_constraint_solving_stage RefinementTest.refine_a_property_of_some_global @@ -157,7 +153,6 @@ TableTests.a_free_shape_can_turn_into_a_scalar_if_it_is_compatible TableTests.a_free_shape_cannot_turn_into_a_scalar_if_it_is_not_compatible TableTests.any_when_indexing_into_an_unsealed_table_with_no_indexer_in_nonstrict_mode TableTests.array_factory_function -TableTests.cannot_augment_sealed_table TableTests.casting_tables_with_props_into_table_with_indexer2 TableTests.casting_tables_with_props_into_table_with_indexer3 TableTests.casting_unsealed_tables_with_props_into_table_with_indexer @@ -181,20 +176,18 @@ TableTests.generalize_table_argument TableTests.generic_table_instantiation_potential_regression TableTests.indexer_on_sealed_table_must_unify_with_free_table TableTests.indexers_get_quantified_too -TableTests.inequality_operators_imply_exactly_matching_types -TableTests.infer_array TableTests.infer_indexer_from_array_like_table TableTests.infer_indexer_from_its_variable_type_and_unifiable TableTests.inferred_return_type_of_free_table TableTests.invariant_table_properties_means_instantiating_tables_in_assignment_is_unsound TableTests.invariant_table_properties_means_instantiating_tables_in_call_is_unsound -TableTests.length_operator_union TableTests.less_exponential_blowup_please TableTests.meta_add TableTests.meta_add_inferred TableTests.metatable_mismatch_should_fail TableTests.missing_metatable_for_sealed_tables_do_not_get_inferred TableTests.mixed_tables_with_implicit_numbered_keys +TableTests.nil_assign_doesnt_hit_indexer TableTests.ok_to_provide_a_subtype_during_construction TableTests.ok_to_set_nil_even_on_non_lvalue_base_expr TableTests.okay_to_add_property_to_unsealed_tables_by_assignment @@ -202,7 +195,6 @@ TableTests.okay_to_add_property_to_unsealed_tables_by_function_call TableTests.only_ascribe_synthetic_names_at_module_scope TableTests.open_table_unification_2 TableTests.parameter_was_set_an_indexer_and_bounded_by_another_parameter -TableTests.parameter_was_set_an_indexer_and_bounded_by_string TableTests.pass_a_union_of_tables_to_a_function_that_requires_a_table TableTests.pass_a_union_of_tables_to_a_function_that_requires_a_table_2 TableTests.persistent_sealed_table_is_immutable @@ -210,7 +202,6 @@ TableTests.quantify_even_that_table_was_never_exported_at_all TableTests.quantify_metatables_of_metatables_of_table TableTests.reasonable_error_when_adding_a_nonexistent_property_to_an_array_like_table TableTests.recursive_metatable_type_call -TableTests.refined_thing_can_be_an_array TableTests.right_table_missing_key2 TableTests.scalar_is_a_subtype_of_a_compatible_polymorphic_shape_type TableTests.scalar_is_not_a_subtype_of_a_compatible_polymorphic_shape_type @@ -228,12 +219,10 @@ TableTests.table_subtyping_with_extra_props_dont_report_multiple_errors TableTests.table_subtyping_with_missing_props_dont_report_multiple_errors2 TableTests.table_unification_4 TableTests.table_unifies_into_map -TableTests.table_writes_introduce_write_properties TableTests.type_mismatch_on_massive_table_is_cut_short TableTests.used_colon_instead_of_dot TableTests.used_dot_instead_of_colon TableTests.when_augmenting_an_unsealed_table_with_an_indexer_apply_the_correct_scope_to_the_indexer_type -TableTests.wrong_assign_does_hit_indexer ToDot.function ToString.exhaustive_toString_of_cyclic_table ToString.free_types @@ -274,6 +263,7 @@ TypeInfer.cli_50041_committing_txnlog_in_apollo_client_error TypeInfer.dont_ice_when_failing_the_occurs_check TypeInfer.dont_report_type_errors_within_an_AstExprError TypeInfer.dont_report_type_errors_within_an_AstStatError +TypeInfer.follow_on_new_types_in_substitution TypeInfer.globals TypeInfer.globals2 TypeInfer.infer_through_group_expr @@ -285,9 +275,10 @@ TypeInfer.type_infer_recursion_limit_no_ice TypeInfer.type_infer_recursion_limit_normalizer TypeInfer.unify_nearly_identical_recursive_types TypeInferAnyError.can_subscript_any -TypeInferAnyError.for_in_loop_iterator_is_error -TypeInferAnyError.for_in_loop_iterator_is_error2 -TypeInferAnyError.replace_every_free_type_when_unifying_a_complex_function_with_any +TypeInferAnyError.for_in_loop_iterator_is_any +TypeInferAnyError.for_in_loop_iterator_is_any2 +TypeInferAnyError.for_in_loop_iterator_is_any_pack +TypeInferAnyError.for_in_loop_iterator_returns_any2 TypeInferClasses.callable_classes TypeInferClasses.cannot_unify_class_instance_with_primitive TypeInferClasses.class_type_mismatch_with_name_conflict @@ -317,10 +308,8 @@ TypeInferFunctions.function_does_not_return_enough_values TypeInferFunctions.function_exprs_are_generalized_at_signature_scope_not_enclosing TypeInferFunctions.function_is_supertype_of_concrete_functions TypeInferFunctions.function_statement_sealed_table_assignment_through_indexer -TypeInferFunctions.fuzzer_missing_follow_in_ast_stat_fun TypeInferFunctions.generic_packs_are_not_variadic TypeInferFunctions.higher_order_function_2 -TypeInferFunctions.higher_order_function_3 TypeInferFunctions.higher_order_function_4 TypeInferFunctions.improved_function_arg_mismatch_error_nonstrict TypeInferFunctions.improved_function_arg_mismatch_errors @@ -338,7 +327,6 @@ TypeInferFunctions.occurs_check_failure_in_function_return_type TypeInferFunctions.other_things_are_not_related_to_function TypeInferFunctions.param_1_and_2_both_takes_the_same_generic_but_their_arguments_are_incompatible TypeInferFunctions.param_1_and_2_both_takes_the_same_generic_but_their_arguments_are_incompatible_2 -TypeInferFunctions.regex_benchmark_string_format_minimization TypeInferFunctions.report_exiting_without_return_nonstrict TypeInferFunctions.return_type_by_overload TypeInferFunctions.tf_suggest_return_type @@ -370,9 +358,7 @@ TypeInferLoops.loop_iter_trailing_nil TypeInferLoops.loop_typecheck_crash_on_empty_optional TypeInferLoops.properly_infer_iteratee_is_a_free_table TypeInferLoops.repeat_loop -TypeInferLoops.varlist_declared_by_for_in_loop_should_be_free TypeInferLoops.while_loop -TypeInferModules.do_not_modify_imported_types_5 TypeInferModules.require TypeInferOOP.dont_suggest_using_colon_rather_than_dot_if_it_wont_help_2 TypeInferOOP.dont_suggest_using_colon_rather_than_dot_if_not_defined_with_colon @@ -396,7 +382,6 @@ TypeInferOperators.typecheck_unary_len_error TypeInferOperators.typecheck_unary_minus_error TypeInferOperators.UnknownGlobalCompoundAssign TypeInferPrimitives.CheckMethodsOfNumber -TypeInferPrimitives.string_index TypeInferUnknownNever.assign_to_local_which_is_never TypeInferUnknownNever.index_on_union_of_tables_for_properties_that_is_never TypeInferUnknownNever.index_on_union_of_tables_for_properties_that_is_sorta_never @@ -414,6 +399,7 @@ TypeSingletons.error_detailed_tagged_union_mismatch_string TypeSingletons.overloaded_function_call_with_singletons_mismatch TypeSingletons.return_type_of_f_is_not_widened TypeSingletons.singletons_stick_around_under_assignment +TypeSingletons.string_singleton_function_call TypeSingletons.table_properties_type_error_escapes TypeSingletons.widen_the_supertype_if_it_is_free_and_subtype_has_singleton TypeStatesTest.typestates_preserve_error_suppression_properties @@ -423,7 +409,6 @@ UnionTypes.generic_function_with_optional_arg UnionTypes.index_on_a_union_type_with_missing_property UnionTypes.less_greedy_unification_with_union_types UnionTypes.optional_arguments_table -UnionTypes.optional_length_error UnionTypes.optional_union_functions UnionTypes.optional_union_members UnionTypes.optional_union_methods