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 1/3] 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 From 041b8ee4e71de42e686cd787d7fe28e7036209eb Mon Sep 17 00:00:00 2001 From: JohnnyMorganz Date: Tue, 4 Jun 2024 22:53:01 +0200 Subject: [PATCH 2/3] Fix edge case in 'findBindingAtPosition' when looking up global binding at start of file (#1254) The 'findBindingAtPosition' AstQuery function can be used to lookup a local or global binding. Inside of this function is a check to "Ignore this binding if we're inside its definition. e.g. local abc = abc -- Will take the definition of abc from outer scope". However, this check is incorrect when we are looking up a global binding at the start of a file. Consider a complete file with the contents: ```lua local x = stri|ng.char(1) ``` and we pass the location of the marker `|` as the position to the find binding position. We will pick up the global binding of the definition `string` coming from a builtin source (either defined via C++ code or a definitions file and loaded into the global scope). The global binding `string` will have a zero position: `0,0,0,0`. However, the `findBindingLocalStatement` check works by looking up the AstAncestry at the binding's defined begin position *in the current source module*. This will then incorrectly return the local statement for `local x`, as that is at the start of the source code. Then in turn, we assume we are in the `local abc = abc` case, and end up skipping over the correct binding. We fix this by checking if the binding is at the global position. If so, we early exit because it is impossible for a global binding to be defined in a local statement. --- Analysis/src/AstQuery.cpp | 6 ++++++ tests/AstQuery.test.cpp | 14 ++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/Analysis/src/AstQuery.cpp b/Analysis/src/AstQuery.cpp index cebb226a..928e5dfb 100644 --- a/Analysis/src/AstQuery.cpp +++ b/Analysis/src/AstQuery.cpp @@ -12,6 +12,7 @@ #include LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution); +LUAU_FASTFLAGVARIABLE(LuauFixBindingForGlobalPos, false); namespace Luau { @@ -332,6 +333,11 @@ std::optional findExpectedTypeAtPosition(const Module& module, const Sou static std::optional findBindingLocalStatement(const SourceModule& source, const Binding& binding) { + // Bindings coming from global sources (e.g., definition files) have a zero position. + // They cannot be defined from a local statement + if (FFlag::LuauFixBindingForGlobalPos && binding.location == Location{{0, 0}, {0, 0}}) + return std::nullopt; + std::vector nodes = findAstAncestryOfPosition(source, binding.location.begin); auto iter = std::find_if(nodes.rbegin(), nodes.rend(), [](AstNode* node) { return node->is(); diff --git a/tests/AstQuery.test.cpp b/tests/AstQuery.test.cpp index 769637a5..c53fe731 100644 --- a/tests/AstQuery.test.cpp +++ b/tests/AstQuery.test.cpp @@ -6,6 +6,8 @@ #include "doctest.h" #include "Fixture.h" +LUAU_FASTFLAG(LuauFixBindingForGlobalPos); + using namespace Luau; struct DocumentationSymbolFixture : BuiltinsFixture @@ -331,4 +333,16 @@ TEST_CASE_FIXTURE(Fixture, "find_expr_ancestry") CHECK(ancestry.back()->is()); } +TEST_CASE_FIXTURE(BuiltinsFixture, "find_binding_at_position_global_start_of_file") +{ + ScopedFastFlag sff{FFlag::LuauFixBindingForGlobalPos, true}; + check("local x = string.char(1)"); + const Position pos(0, 12); + + std::optional binding = findBindingAtPosition(*getMainModule(), *getMainSourceModule(), pos); + + REQUIRE(binding); + CHECK_EQ(binding->location, Location{Position{0, 0}, Position{0, 0}}); +} + TEST_SUITE_END(); From 43bf7c4e051b0d49dbb2bd3cbb2471d235da55db Mon Sep 17 00:00:00 2001 From: Jack <85714123+jackdotink@users.noreply.github.com> Date: Wed, 5 Jun 2024 09:52:30 -0500 Subject: [PATCH 3/3] implement leading bar and ampersand in types (#1286) Implements the [Leading `|` and `&` in types](https://rfcs.luau-lang.org/syntax-leading-bar-and-ampersand.html) RFC. The changes to the parser are exactly as described in the RFC. --------- Co-authored-by: Alexander McCord <11488393+alexmccord@users.noreply.github.com> --- Ast/src/Parser.cpp | 34 +++++++++++++++++++++++++++++----- tests/Parser.test.cpp | 23 +++++++++++++++++++++++ 2 files changed, 52 insertions(+), 5 deletions(-) diff --git a/Ast/src/Parser.cpp b/Ast/src/Parser.cpp index e26df1fa..5ca480e8 100644 --- a/Ast/src/Parser.cpp +++ b/Ast/src/Parser.cpp @@ -17,6 +17,7 @@ LUAU_FASTINTVARIABLE(LuauParseErrorLimit, 100) // flag so that we don't break production games by reverting syntax changes. // See docs/SyntaxChanges.md for an explanation. LUAU_FASTFLAGVARIABLE(DebugLuauDeferredConstraintResolution, false) +LUAU_FASTFLAGVARIABLE(LuauLeadingBarAndAmpersand, false) namespace Luau { @@ -1523,7 +1524,11 @@ AstType* Parser::parseFunctionTypeTail(const Lexeme& begin, AstArray parts(scratchType); - parts.push_back(type); + + if (!FFlag::LuauLeadingBarAndAmpersand || type != nullptr) + { + parts.push_back(type); + } incrementRecursionCounter("type annotation"); @@ -1623,15 +1628,34 @@ AstTypeOrPack Parser::parseTypeOrPack() AstType* Parser::parseType(bool inDeclarationContext) { unsigned int oldRecursionCount = recursionCounter; - // recursion counter is incremented in parseSimpleType + // recursion counter is incremented in parseSimpleType and/or parseTypeSuffix Location begin = lexer.current().location; - AstType* type = parseSimpleType(/* allowPack= */ false, /* in declaration context */ inDeclarationContext).type; + if (FFlag::LuauLeadingBarAndAmpersand) + { + AstType* type = nullptr; - recursionCounter = oldRecursionCount; + Lexeme::Type c = lexer.current().type; + if (c != '|' && c != '&') + { + type = parseSimpleType(/* allowPack= */ false, /* in declaration context */ inDeclarationContext).type; + recursionCounter = oldRecursionCount; + } - return parseTypeSuffix(type, begin); + AstType* typeWithSuffix = parseTypeSuffix(type, begin); + recursionCounter = oldRecursionCount; + + return typeWithSuffix; + } + else + { + AstType* type = parseSimpleType(/* allowPack= */ false, /* in declaration context */ inDeclarationContext).type; + + recursionCounter = oldRecursionCount; + + return parseTypeSuffix(type, begin); + } } // Type ::= nil | Name[`.' Name] [ `<' Type [`,' ...] `>' ] | `typeof' `(' expr `)' | `{' [PropList] `}' diff --git a/tests/Parser.test.cpp b/tests/Parser.test.cpp index b178f539..6b4bcf22 100644 --- a/tests/Parser.test.cpp +++ b/tests/Parser.test.cpp @@ -16,6 +16,7 @@ LUAU_FASTINT(LuauRecursionLimit); LUAU_FASTINT(LuauTypeLengthLimit); LUAU_FASTINT(LuauParseErrorLimit); LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution); +LUAU_FASTFLAG(LuauLeadingBarAndAmpersand); namespace { @@ -3167,4 +3168,26 @@ TEST_CASE_FIXTURE(Fixture, "read_write_table_properties") LUAU_ASSERT(pr.errors.size() == 0); } +TEST_CASE_FIXTURE(Fixture, "can_parse_leading_bar_unions_successfully") +{ + ScopedFastFlag sff{FFlag::LuauLeadingBarAndAmpersand, true}; + + parse(R"(type A = | "Hello" | "World")"); +} + +TEST_CASE_FIXTURE(Fixture, "can_parse_leading_ampersand_intersections_successfully") +{ + ScopedFastFlag sff{FFlag::LuauLeadingBarAndAmpersand, true}; + + parse(R"(type A = & { string } & { number })"); +} + +TEST_CASE_FIXTURE(Fixture, "mixed_leading_intersection_and_union_not_allowed") +{ + ScopedFastFlag sff{FFlag::LuauLeadingBarAndAmpersand, true}; + + matchParseError("type A = & number | string | boolean", "Mixing union and intersection types is not allowed; consider wrapping in parentheses."); + matchParseError("type A = | number & string & boolean", "Mixing union and intersection types is not allowed; consider wrapping in parentheses."); +} + TEST_SUITE_END();