diff --git a/Analysis/include/Luau/Constraint.h b/Analysis/include/Luau/Constraint.h index a026fdae..bba3fced 100644 --- a/Analysis/include/Luau/Constraint.h +++ b/Analysis/include/Luau/Constraint.h @@ -49,8 +49,8 @@ struct InstantiationConstraint TypeId superType; }; -// iteratee is iterable -// iterators is the iteration types. +// variables ~ iterate iterator +// Unpack the iterator, figure out what types it iterates over, and bind those types to variables. struct IterableConstraint { TypePackId iterator; diff --git a/Analysis/include/Luau/ConstraintGenerator.h b/Analysis/include/Luau/ConstraintGenerator.h index 69b0fd20..a3e1092f 100644 --- a/Analysis/include/Luau/ConstraintGenerator.h +++ b/Analysis/include/Luau/ConstraintGenerator.h @@ -225,6 +225,7 @@ private: Inference check(const ScopePtr& scope, AstExprConstantBool* bool_, std::optional expectedType, bool forceSingleton); Inference check(const ScopePtr& scope, AstExprLocal* local); Inference check(const ScopePtr& scope, AstExprGlobal* global); + Inference checkIndexName(const ScopePtr& scope, const RefinementKey* key, AstExpr* indexee, std::string index); Inference check(const ScopePtr& scope, AstExprIndexName* indexName); Inference check(const ScopePtr& scope, AstExprIndexExpr* indexExpr); Inference check(const ScopePtr& scope, AstExprFunction* func, std::optional expectedType, bool generalize); diff --git a/Analysis/include/Luau/Error.h b/Analysis/include/Luau/Error.h index ddbf6dcb..06ea9601 100644 --- a/Analysis/include/Luau/Error.h +++ b/Analysis/include/Luau/Error.h @@ -372,13 +372,21 @@ struct CheckedFunctionCallError bool operator==(const CheckedFunctionCallError& rhs) const; }; +struct NonStrictFunctionDefinitionError +{ + std::string functionName; + std::string argument; + TypeId argumentType; + bool operator==(const NonStrictFunctionDefinitionError& rhs) const; +}; + using TypeErrorData = Variant; + UninhabitedTypePackFamily, WhereClauseNeeded, PackWhereClauseNeeded, CheckedFunctionCallError, NonStrictFunctionDefinitionError>; struct TypeErrorSummary { diff --git a/Analysis/include/Luau/Subtyping.h b/Analysis/include/Luau/Subtyping.h index 926ffc9c..d2619fe2 100644 --- a/Analysis/include/Luau/Subtyping.h +++ b/Analysis/include/Luau/Subtyping.h @@ -32,12 +32,17 @@ enum class SubtypingVariance // Used for an empty key. Should never appear in actual code. Invalid, Covariant, + // This is used to identify cases where we have a covariant + a + // contravariant reason and we need to merge them. + Contravariant, Invariant, }; struct SubtypingReasoning { + // The path, relative to the _root subtype_, where subtyping failed. Path subPath; + // The path, relative to the _root supertype_, where subtyping failed. Path superPath; SubtypingVariance variance = SubtypingVariance::Covariant; @@ -49,6 +54,9 @@ struct SubtypingReasoningHash size_t operator()(const SubtypingReasoning& r) const; }; +using SubtypingReasonings = DenseHashSet; +static const SubtypingReasoning kEmptyReasoning = SubtypingReasoning{TypePath::kEmpty, TypePath::kEmpty, SubtypingVariance::Invalid}; + struct SubtypingResult { bool isSubtype = false; @@ -58,8 +66,7 @@ struct SubtypingResult /// The reason for isSubtype to be false. May not be present even if /// isSubtype is false, depending on the input types. - DenseHashSet reasoning{ - SubtypingReasoning{TypePath::kEmpty, TypePath::kEmpty, SubtypingVariance::Invalid}}; + SubtypingReasonings reasoning{kEmptyReasoning}; SubtypingResult& andAlso(const SubtypingResult& other); SubtypingResult& orElse(const SubtypingResult& other); @@ -69,7 +76,6 @@ struct SubtypingResult SubtypingResult& withBothPath(TypePath::Path path); SubtypingResult& withSubPath(TypePath::Path path); SubtypingResult& withSuperPath(TypePath::Path path); - SubtypingResult& withVariance(SubtypingVariance variance); // Only negates the `isSubtype`. static SubtypingResult negate(const SubtypingResult& result); diff --git a/Analysis/include/Luau/Unifier2.h b/Analysis/include/Luau/Unifier2.h index 49a275d5..4930df6f 100644 --- a/Analysis/include/Luau/Unifier2.h +++ b/Analysis/include/Luau/Unifier2.h @@ -61,7 +61,7 @@ struct Unifier2 bool unify(TypeId subTy, const UnionType* superUnion); bool unify(const IntersectionType* subIntersection, TypeId superTy); bool unify(TypeId subTy, const IntersectionType* superIntersection); - bool unify(const TableType* subTable, const TableType* superTable); + bool unify(TableType* subTable, const TableType* superTable); bool unify(const MetatableType* subMetatable, const MetatableType* superMetatable); // TODO think about this one carefully. We don't do unions or intersections of type packs diff --git a/Analysis/src/ConstraintGenerator.cpp b/Analysis/src/ConstraintGenerator.cpp index 12e4e7da..cdfe9a7f 100644 --- a/Analysis/src/ConstraintGenerator.cpp +++ b/Analysis/src/ConstraintGenerator.cpp @@ -750,17 +750,18 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatForIn* forI for (AstLocal* var : forIn->vars) { - TypeId ty = nullptr; - if (var->annotation) - ty = resolveType(loopScope, var->annotation, /*inTypeArguments*/ false); - else - ty = freshType(loopScope); - - loopScope->bindings[var] = Binding{ty, var->location}; - TypeId assignee = arena->addType(BlockedType{}); variableTypes.push_back(assignee); + if (var->annotation) + { + TypeId annotationTy = resolveType(loopScope, var->annotation, /*inTypeArguments*/ false); + loopScope->bindings[var] = Binding{annotationTy, var->location}; + addConstraint(scope, var->location, SubtypeConstraint{assignee, annotationTy}); + } + else + loopScope->bindings[var] = Binding{assignee, var->location}; + DefId def = dfg->getDef(var); loopScope->lvalueTypes[def] = assignee; } @@ -1439,9 +1440,6 @@ InferencePack ConstraintGenerator::checkPack(const ScopePtr& scope, AstExprCall* module->astOriginalCallTypes[call->func] = fnType; module->astOriginalCallTypes[call] = fnType; - TypeId instantiatedFnType = arena->addType(BlockedType{}); - addConstraint(scope, call->location, InstantiationConstraint{instantiatedFnType, fnType}); - Checkpoint argBeginCheckpoint = checkpoint(this); std::vector args; @@ -1740,12 +1738,11 @@ Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprGlobal* globa } } -Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprIndexName* indexName) +Inference ConstraintGenerator::checkIndexName(const ScopePtr& scope, const RefinementKey* key, AstExpr* indexee, std::string index) { - TypeId obj = check(scope, indexName->expr).ty; + TypeId obj = check(scope, indexee).ty; TypeId result = arena->addType(BlockedType{}); - const RefinementKey* key = dfg->getRefinementKey(indexName); if (key) { if (auto ty = lookup(scope.get(), key->def)) @@ -1754,7 +1751,7 @@ Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprIndexName* in scope->rvalueRefinements[key->def] = result; } - addConstraint(scope, indexName->expr->location, HasPropConstraint{result, obj, indexName->index.value}); + addConstraint(scope, indexee->location, HasPropConstraint{result, obj, std::move(index)}); if (key) return Inference{result, refinementArena.proposition(key, builtinTypes->truthyType)}; @@ -1762,10 +1759,23 @@ Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprIndexName* in return Inference{result}; } +Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprIndexName* indexName) +{ + const RefinementKey* key = dfg->getRefinementKey(indexName); + return checkIndexName(scope, key, indexName->expr, indexName->index.value); +} + Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprIndexExpr* indexExpr) { + if (auto constantString = indexExpr->index->as()) + { + const RefinementKey* key = dfg->getRefinementKey(indexExpr); + return checkIndexName(scope, key, indexExpr->expr, constantString->value.data); + } + TypeId obj = check(scope, indexExpr->expr).ty; TypeId indexType = check(scope, indexExpr->index).ty; + TypeId result = freshType(scope); const RefinementKey* key = dfg->getRefinementKey(indexExpr); @@ -3079,15 +3089,23 @@ struct GlobalPrepopulator : AstVisitor { } + bool visit(AstExprGlobal* global) override + { + if (auto ty = globalScope->lookup(global->name)) + { + DefId def = dfg->getDef(global); + globalScope->lvalueTypes[def] = *ty; + } + + return true; + } + bool visit(AstStatFunction* function) override { if (AstExprGlobal* g = function->name->as()) { TypeId bt = arena->addType(BlockedType{}); globalScope->bindings[g->name] = Binding{bt}; - - DefId def = dfg->getDef(function->name); - globalScope->lvalueTypes[def] = bt; } return true; diff --git a/Analysis/src/ConstraintSolver.cpp b/Analysis/src/ConstraintSolver.cpp index de2f566d..fa0f767b 100644 --- a/Analysis/src/ConstraintSolver.cpp +++ b/Analysis/src/ConstraintSolver.cpp @@ -1263,9 +1263,6 @@ bool ConstraintSolver::tryDispatch(const SetPropConstraint& c, NotNull(subjectType)) - return block(subjectType, constraint); - std::optional existingPropType = subjectType; for (const std::string& segment : c.path) { @@ -1301,25 +1298,13 @@ bool ConstraintSolver::tryDispatch(const SetPropConstraint& c, NotNull(subjectType)) { - TypeId ty = freshType(arena, builtinTypes, constraint->scope); - - // Mint a chain of free tables per c.path - for (auto it = rbegin(c.path); it != rend(c.path); ++it) - { - TableType t{TableState::Free, TypeLevel{}, constraint->scope}; - t.props[*it] = {ty}; - - ty = arena->addType(std::move(t)); - } - - LUAU_ASSERT(ty); - - bind(subjectType, ty); - if (follow(c.resultType) != follow(ty)) - bind(c.resultType, ty); - unblock(subjectType, constraint->location); - unblock(c.resultType, constraint->location); - return true; + /* + * This should never occur because lookupTableProp() will add bounds to + * any free types it encounters. There will always be an + * existingPropType if the subject is free. + */ + LUAU_ASSERT(false); + return false; } else if (auto ttv = getMutable(subjectType)) { @@ -1328,7 +1313,7 @@ bool ConstraintSolver::tryDispatch(const SetPropConstraint& c, NotNullpersistent); ttv->props[c.path[0]] = Property{c.propType}; - bind(c.resultType, c.subjectType); + bind(c.resultType, subjectType); unblock(c.resultType, constraint->location); return true; } @@ -1337,26 +1322,12 @@ bool ConstraintSolver::tryDispatch(const SetPropConstraint& c, NotNullpersistent); updateTheTableType(builtinTypes, NotNull{arena}, subjectType, c.path, c.propType); - bind(c.resultType, c.subjectType); - unblock(subjectType, constraint->location); - unblock(c.resultType, constraint->location); - return true; - } - else - { - bind(c.resultType, subjectType); - unblock(c.resultType, constraint->location); - return true; } } - else - { - // Other kinds of types don't change shape when properties are assigned - // to them. (if they allow properties at all!) - bind(c.resultType, subjectType); - unblock(c.resultType, constraint->location); - return true; - } + + bind(c.resultType, subjectType); + unblock(c.resultType, constraint->location); + return true; } bool ConstraintSolver::tryDispatch(const SetIndexerConstraint& c, NotNull constraint, bool force) @@ -1908,6 +1879,7 @@ bool ConstraintSolver::tryDispatchIterableFunction( TypeId retIndex; if (isNil(firstIndexTy) || isOptional(firstIndexTy)) { + // FIXME freshType is suspect here firstIndex = arena->addType(UnionType{{freshType(arena, builtinTypes, constraint->scope), builtinTypes->nilType}}); retIndex = firstIndex; } @@ -1949,7 +1921,7 @@ bool ConstraintSolver::tryDispatchIterableFunction( modifiedNextRetHead.push_back(*it); TypePackId modifiedNextRetPack = arena->addTypePack(std::move(modifiedNextRetHead), it.tail()); - auto psc = pushConstraint(constraint->scope, constraint->location, PackSubtypeConstraint{c.variables, modifiedNextRetPack}); + auto psc = pushConstraint(constraint->scope, constraint->location, UnpackConstraint{c.variables, modifiedNextRetPack}); inheritBlocks(constraint, psc); return true; diff --git a/Analysis/src/Error.cpp b/Analysis/src/Error.cpp index 3be63f02..5ec2d52b 100644 --- a/Analysis/src/Error.cpp +++ b/Analysis/src/Error.cpp @@ -533,6 +533,12 @@ struct ErrorConverter return "Function '" + e.checkedFunctionName + "' expects '" + toString(e.expected) + "' at argument #" + std::to_string(e.argumentIndex) + ", but got '" + Luau::toString(e.passed) + "'"; } + + std::string operator()(const NonStrictFunctionDefinitionError& e) const + { + return "Argument " + e.argument + " with type '" + toString(e.argumentType) + "' in function '" + e.functionName + + "' is used in a way that will run time error"; + } }; struct InvalidNameChecker @@ -861,6 +867,11 @@ bool CheckedFunctionCallError::operator==(const CheckedFunctionCallError& rhs) c argumentIndex == rhs.argumentIndex; } +bool NonStrictFunctionDefinitionError::operator==(const NonStrictFunctionDefinitionError& rhs) const +{ + return functionName == rhs.functionName && argument == rhs.argument && argumentType == rhs.argumentType; +} + std::string toString(const TypeError& error) { return toString(error, TypeErrorToStringOptions{}); @@ -1032,6 +1043,10 @@ void copyError(T& e, TypeArena& destArena, CloneState& cloneState) e.expected = clone(e.expected); e.passed = clone(e.passed); } + else if constexpr (std::is_same_v) + { + e.argumentType = clone(e.argumentType); + } else static_assert(always_false_v, "Non-exhaustive type switch"); } diff --git a/Analysis/src/IostreamHelpers.cpp b/Analysis/src/IostreamHelpers.cpp index 7a58a244..85a03b48 100644 --- a/Analysis/src/IostreamHelpers.cpp +++ b/Analysis/src/IostreamHelpers.cpp @@ -204,6 +204,9 @@ static void errorToString(std::ostream& stream, const T& err) else if constexpr (std::is_same_v) stream << "CheckedFunctionCallError { expected = '" << toString(err.expected) << "', passed = '" << toString(err.passed) << "', checkedFunctionName = " << err.checkedFunctionName << ", argumentIndex = " << std::to_string(err.argumentIndex) << " }"; + else if constexpr (std::is_same_v) + stream << "NonStrictFunctionDefinitionError { functionName = '" + err.functionName + "', argument = '" + err.argument + + "', argumentType = '" + toString(err.argumentType) + "' }"; else static_assert(always_false_v, "Non-exhaustive type switch"); } diff --git a/Analysis/src/NonStrictTypeChecker.cpp b/Analysis/src/NonStrictTypeChecker.cpp index 451fa8f6..2372a9a7 100644 --- a/Analysis/src/NonStrictTypeChecker.cpp +++ b/Analysis/src/NonStrictTypeChecker.cpp @@ -14,6 +14,7 @@ #include "Luau/Def.h" #include +#include namespace Luau { @@ -105,9 +106,10 @@ struct NonStrictContext return conj; } - void removeFromContext(const std::vector& defs) + // Returns true if the removal was successful + bool remove(const DefId& def) { - // TODO: unimplemented + return context.erase(def.get()) == 1; } std::optional find(const DefId& def) const @@ -138,6 +140,7 @@ struct NonStrictTypeChecker NotNull dfg; DenseHashSet noTypeFamilyErrors{nullptr}; std::vector> stack; + DenseHashMap cachedNegations{nullptr}; const NotNull limits; @@ -271,8 +274,22 @@ struct NonStrictTypeChecker { auto StackPusher = pushStack(block); NonStrictContext ctx; - for (AstStat* statement : block->body) - ctx = NonStrictContext::disjunction(builtinTypes, NotNull{&arena}, ctx, visit(statement)); + + + for (auto it = block->body.rbegin(); it != block->body.rend(); it++) + { + AstStat* stat = *it; + if (AstStatLocal* local = stat->as()) + { + // Iterating in reverse order + // local x ; B generates the context of B without x + visit(local); + for (auto local : local->vars) + ctx.remove(dfg->getDef(local)); + } + else + ctx = NonStrictContext::disjunction(builtinTypes, NotNull{&arena}, visit(stat), ctx); + } return ctx; } @@ -505,9 +522,7 @@ struct NonStrictTypeChecker AstExpr* arg = call->args.data[i]; TypeId expectedArgType = argTypes[i]; DefId def = dfg->getDef(arg); - // TODO: Cache negations created here!!! - // See Jira Ticket: https://roblox.atlassian.net/browse/CLI-87539 - TypeId runTimeErrorTy = arena.addType(NegationType{expectedArgType}); + TypeId runTimeErrorTy = getOrCreateNegation(expectedArgType); fresh.context[def.get()] = runTimeErrorTy; } @@ -537,8 +552,16 @@ struct NonStrictTypeChecker NonStrictContext visit(AstExprFunction* exprFn) { + // TODO: should a function being used as an expression generate a context without the arguments? auto pusher = pushStack(exprFn); - return visit(exprFn->body); + NonStrictContext remainder = visit(exprFn->body); + for (AstLocal* local : exprFn->args) + { + if (std::optional ty = willRunTimeErrorFunctionDefinition(local, remainder)) + reportError(NonStrictFunctionDefinitionError{exprFn->debugname.value, local->name.value, *ty}, local->location); + remainder.remove(dfg->getDef(local)); + } + return remainder; } NonStrictContext visit(AstExprTable* table) @@ -603,6 +626,31 @@ struct NonStrictTypeChecker return {}; } + + std::optional willRunTimeErrorFunctionDefinition(AstLocal* fragment, const NonStrictContext& context) + { + DefId def = dfg->getDef(fragment); + if (std::optional contextTy = context.find(def)) + { + SubtypingResult r1 = subtyping.isSubtype(builtinTypes->unknownType, *contextTy); + SubtypingResult r2 = subtyping.isSubtype(*contextTy, builtinTypes->unknownType); + if (r1.normalizationTooComplex || r2.normalizationTooComplex) + reportError(NormalizationTooComplex{}, fragment->location); + bool isUnknown = r1.isSubtype && r2.isSubtype; + if (isUnknown) + return {builtinTypes->unknownType}; + } + return {}; + } + +private: + TypeId getOrCreateNegation(TypeId baseType) + { + TypeId& cachedResult = cachedNegations[baseType]; + if (!cachedResult) + cachedResult = arena.addType(NegationType{baseType}); + return cachedResult; + }; }; void checkNonStrict(NotNull builtinTypes, NotNull ice, NotNull unifierState, diff --git a/Analysis/src/Normalize.cpp b/Analysis/src/Normalize.cpp index 3c961047..30ab7895 100644 --- a/Analysis/src/Normalize.cpp +++ b/Analysis/src/Normalize.cpp @@ -2772,7 +2772,7 @@ bool Normalizer::intersectNormalWithTy(NormalizedType& here, TypeId there, Set(there) || get(there) || get(there) || get(there) || - get(there)) + get(there) || get(there)) { NormalizedType thereNorm{builtinTypes}; NormalizedType topNorm{builtinTypes}; @@ -2780,6 +2780,10 @@ bool Normalizer::intersectNormalWithTy(NormalizedType& here, TypeId there, Set(std::move(topNorm))); return intersectNormals(here, thereNorm); } + else if (auto lt = get(there)) + { + return intersectNormalWithTy(here, lt->domain, seenSetTypes); + } NormalizedTyvars tyvars = std::move(here.tyvars); diff --git a/Analysis/src/Subtyping.cpp b/Analysis/src/Subtyping.cpp index 7e7c8cd6..49db9cd3 100644 --- a/Analysis/src/Subtyping.cpp +++ b/Analysis/src/Subtyping.cpp @@ -16,6 +16,8 @@ #include +LUAU_FASTFLAGVARIABLE(DebugLuauSubtypingCheckPathValidity, false); + namespace Luau { @@ -55,16 +57,69 @@ size_t SubtypingReasoningHash::operator()(const SubtypingReasoning& r) const return TypePath::PathHash()(r.subPath) ^ (TypePath::PathHash()(r.superPath) << 1) ^ (static_cast(r.variance) << 1); } +template +static void assertReasoningValid(TID subTy, TID superTy, const SubtypingResult& result, NotNull builtinTypes) +{ + if (!FFlag::DebugLuauSubtypingCheckPathValidity) + return; + + for (const SubtypingReasoning& reasoning : result.reasoning) + { + LUAU_ASSERT(traverse(subTy, reasoning.subPath, builtinTypes)); + LUAU_ASSERT(traverse(superTy, reasoning.superPath, builtinTypes)); + } +} + +template<> +void assertReasoningValid(TableIndexer subIdx, TableIndexer superIdx, const SubtypingResult& result, NotNull builtinTypes) +{ + // Empty method to satisfy the compiler. +} + +static SubtypingReasonings mergeReasonings(const SubtypingReasonings& a, const SubtypingReasonings& b) +{ + SubtypingReasonings result{kEmptyReasoning}; + + for (const SubtypingReasoning& r : a) + { + if (r.variance == SubtypingVariance::Invariant) + result.insert(r); + else if (r.variance == SubtypingVariance::Covariant || r.variance == SubtypingVariance::Contravariant) + { + SubtypingReasoning inverseReasoning = SubtypingReasoning{ + r.subPath, r.superPath, r.variance == SubtypingVariance::Covariant ? SubtypingVariance::Contravariant : SubtypingVariance::Covariant}; + if (b.contains(inverseReasoning)) + result.insert(SubtypingReasoning{r.subPath, r.superPath, SubtypingVariance::Invariant}); + else + result.insert(r); + } + } + + for (const SubtypingReasoning& r : b) + { + if (r.variance == SubtypingVariance::Invariant) + result.insert(r); + else if (r.variance == SubtypingVariance::Covariant || r.variance == SubtypingVariance::Contravariant) + { + SubtypingReasoning inverseReasoning = SubtypingReasoning{ + r.subPath, r.superPath, r.variance == SubtypingVariance::Covariant ? SubtypingVariance::Contravariant : SubtypingVariance::Covariant}; + if (a.contains(inverseReasoning)) + result.insert(SubtypingReasoning{r.subPath, r.superPath, SubtypingVariance::Invariant}); + else + result.insert(r); + } + } + + return result; +} + SubtypingResult& SubtypingResult::andAlso(const SubtypingResult& other) { // If the other result is not a subtype, we want to join all of its // reasonings to this one. If this result already has reasonings of its own, // those need to be attributed here. if (!other.isSubtype) - { - for (const SubtypingReasoning& r : other.reasoning) - reasoning.insert(r); - } + reasoning = mergeReasonings(reasoning, other.reasoning); isSubtype &= other.isSubtype; // `|=` is intentional here, we want to preserve error related flags. @@ -86,10 +141,7 @@ SubtypingResult& SubtypingResult::orElse(const SubtypingResult& other) if (other.isSubtype) reasoning.clear(); else - { - for (const SubtypingReasoning& r : other.reasoning) - reasoning.insert(r); - } + reasoning = mergeReasonings(reasoning, other.reasoning); } isSubtype |= other.isSubtype; @@ -162,19 +214,6 @@ SubtypingResult& SubtypingResult::withSuperPath(TypePath::Path path) return *this; } -SubtypingResult& SubtypingResult::withVariance(SubtypingVariance variance) -{ - if (reasoning.empty()) - reasoning.insert(SubtypingReasoning{TypePath::kEmpty, TypePath::kEmpty, variance}); - else - { - for (auto& r : reasoning) - r.variance = variance; - } - - return *this; -} - SubtypingResult SubtypingResult::negate(const SubtypingResult& result) { return SubtypingResult{ @@ -245,7 +284,10 @@ SubtypingResult Subtyping::isSubtype(TypeId subTy, TypeId superTy) result.isSubtype = false; } - result.andAlso(isCovariantWith(env, lowerBound, upperBound)); + SubtypingResult boundsResult = isCovariantWith(env, lowerBound, upperBound); + boundsResult.reasoning.clear(); + + result.andAlso(boundsResult); } /* TODO: We presently don't store subtype test results in the persistent @@ -370,7 +412,10 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypeId sub { SubtypingResult semantic = isCovariantWith(env, normalizer->normalize(subTy), normalizer->normalize(superTy)); if (semantic.isSubtype) + { + semantic.reasoning.clear(); result = semantic; + } } } else if (auto superIntersection = get(superTy)) @@ -382,7 +427,12 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypeId sub { SubtypingResult semantic = isCovariantWith(env, normalizer->normalize(subTy), normalizer->normalize(superTy)); if (semantic.isSubtype) + { + // Clear the semantic reasoning, as any reasonings within + // potentially contain invalid paths. + semantic.reasoning.clear(); result = semantic; + } } } else if (get(superTy)) @@ -411,9 +461,31 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypeId sub else if (auto p = get2(subTy, superTy)) result = isCovariantWith(env, p.first->ty, p.second->ty).withBothComponent(TypePath::TypeField::Negated); else if (auto subNegation = get(subTy)) + { result = isCovariantWith(env, subNegation, superTy); + if (!result.isSubtype && !result.isErrorSuppressing && !result.normalizationTooComplex) + { + SubtypingResult semantic = isCovariantWith(env, normalizer->normalize(subTy), normalizer->normalize(superTy)); + if (semantic.isSubtype) + { + semantic.reasoning.clear(); + result = semantic; + } + } + } else if (auto superNegation = get(superTy)) + { result = isCovariantWith(env, subTy, superNegation); + if (!result.isSubtype && !result.isErrorSuppressing && !result.normalizationTooComplex) + { + SubtypingResult semantic = isCovariantWith(env, normalizer->normalize(subTy), normalizer->normalize(superTy)); + if (semantic.isSubtype) + { + semantic.reasoning.clear(); + result = semantic; + } + } + } else if (auto subGeneric = get(subTy); subGeneric && variance == Variance::Covariant) { bool ok = bindGeneric(env, subTy, superTy); @@ -449,6 +521,8 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypeId sub else if (auto p = get2(subTy, superTy)) result = isCovariantWith(env, p); + assertReasoningValid(subTy, superTy, result, builtinTypes); + return cache(env, result, subTy, superTy); } @@ -536,7 +610,7 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypePackId for (size_t i = headSize; i < subHead.size(); ++i) results.push_back(isCovariantWith(env, subHead[i], vt->ty) .withSubComponent(TypePath::Index{i}) - .withSuperComponent(TypePath::TypeField::Variadic)); + .withSuperPath(TypePath::PathBuilder().tail().variadic().build())); } else if (auto gt = get(*superTail)) { @@ -664,19 +738,38 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypePackId iceReporter->ice("Subtyping test encountered the unexpected type pack: " + toString(*superTail)); } - return SubtypingResult::all(results); + SubtypingResult result = SubtypingResult::all(results); + assertReasoningValid(subTp, superTp, result, builtinTypes); + + return result; } template SubtypingResult Subtyping::isContravariantWith(SubtypingEnvironment& env, SubTy&& subTy, SuperTy&& superTy) { SubtypingResult result = isCovariantWith(env, superTy, subTy); - // If we don't swap the paths here, we will end up producing an invalid path - // whenever we involve contravariance. We'll end up appending path - // components that should belong to the supertype to the subtype, and vice - // versa. - for (auto& reasoning : result.reasoning) - std::swap(reasoning.subPath, reasoning.superPath); + if (result.reasoning.empty()) + result.reasoning.insert(SubtypingReasoning{TypePath::kEmpty, TypePath::kEmpty, SubtypingVariance::Contravariant}); + else + { + // If we don't swap the paths here, we will end up producing an invalid path + // whenever we involve contravariance. We'll end up appending path + // components that should belong to the supertype to the subtype, and vice + // versa. + for (auto& reasoning : result.reasoning) + { + std::swap(reasoning.subPath, reasoning.superPath); + + // Also swap covariant/contravariant, since those are also the other way + // around. + if (reasoning.variance == SubtypingVariance::Covariant) + reasoning.variance = SubtypingVariance::Contravariant; + else if (reasoning.variance == SubtypingVariance::Contravariant) + reasoning.variance = SubtypingVariance::Covariant; + } + } + + assertReasoningValid(subTy, superTy, result, builtinTypes); return result; } @@ -684,7 +777,17 @@ SubtypingResult Subtyping::isContravariantWith(SubtypingEnvironment& env, SubTy& template SubtypingResult Subtyping::isInvariantWith(SubtypingEnvironment& env, SubTy&& subTy, SuperTy&& superTy) { - return isCovariantWith(env, subTy, superTy).andAlso(isContravariantWith(env, subTy, superTy)).withVariance(SubtypingVariance::Invariant); + SubtypingResult result = isCovariantWith(env, subTy, superTy).andAlso(isContravariantWith(env, subTy, superTy)); + if (result.reasoning.empty()) + result.reasoning.insert(SubtypingReasoning{TypePath::kEmpty, TypePath::kEmpty, SubtypingVariance::Invariant}); + else + { + for (auto& reasoning : result.reasoning) + reasoning.variance = SubtypingVariance::Invariant; + } + + assertReasoningValid(subTy, superTy, result, builtinTypes); + return result; } template @@ -696,13 +799,13 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const TryP template SubtypingResult Subtyping::isContravariantWith(SubtypingEnvironment& env, const TryPair& pair) { - return isCovariantWith(env, pair.second, pair.first); + return isContravariantWith(env, pair.first, pair.second); } template SubtypingResult Subtyping::isInvariantWith(SubtypingEnvironment& env, const TryPair& pair) { - return isCovariantWith(env, pair).andAlso(isContravariantWith(pair)).withVariance(SubtypingVariance::Invariant); + return isInvariantWith(env, pair.first, pair.second); } /* @@ -788,17 +891,17 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const Nega if (is(negatedTy)) { // ¬never ~ unknown - result = isCovariantWith(env, builtinTypes->unknownType, superTy); + result = isCovariantWith(env, builtinTypes->unknownType, superTy).withSubComponent(TypePath::TypeField::Negated); } else if (is(negatedTy)) { // ¬unknown ~ never - result = isCovariantWith(env, builtinTypes->neverType, superTy); + result = isCovariantWith(env, builtinTypes->neverType, superTy).withSubComponent(TypePath::TypeField::Negated); } else if (is(negatedTy)) { // ¬any ~ any - result = isCovariantWith(env, negatedTy, superTy); + result = isCovariantWith(env, negatedTy, superTy).withSubComponent(TypePath::TypeField::Negated); } else if (auto u = get(negatedTy)) { @@ -808,8 +911,13 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const Nega for (TypeId ty : u) { - NegationType negatedTmp{ty}; - subtypings.push_back(isCovariantWith(env, &negatedTmp, superTy)); + if (auto negatedPart = get(follow(ty))) + subtypings.push_back(isCovariantWith(env, negatedPart->ty, superTy).withSubComponent(TypePath::TypeField::Negated)); + else + { + NegationType negatedTmp{ty}; + subtypings.push_back(isCovariantWith(env, &negatedTmp, superTy)); + } } result = SubtypingResult::all(subtypings); @@ -823,7 +931,7 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const Nega for (TypeId ty : i) { if (auto negatedPart = get(follow(ty))) - subtypings.push_back(isCovariantWith(env, negatedPart->ty, superTy)); + subtypings.push_back(isCovariantWith(env, negatedPart->ty, superTy).withSubComponent(TypePath::TypeField::Negated)); else { NegationType negatedTmp{ty}; @@ -841,10 +949,10 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const Nega // subtype of other stuff. else { - result = {false}; + result = SubtypingResult{false}.withSubComponent(TypePath::TypeField::Negated); } - return result.withSubComponent(TypePath::TypeField::Negated); + return result; } SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const TypeId subTy, const NegationType* superNegation) @@ -885,7 +993,7 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const Type } } - result = SubtypingResult::all(subtypings); + return SubtypingResult::all(subtypings); } else if (auto i = get(negatedTy)) { @@ -904,7 +1012,7 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const Type } } - result = SubtypingResult::any(subtypings); + return SubtypingResult::any(subtypings); } else if (auto p = get2(subTy, negatedTy)) { @@ -986,8 +1094,7 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const Tabl { std::vector results; if (auto it = subTable->props.find(name); it != subTable->props.end()) - results.push_back(isInvariantWith(env, it->second.type(), prop.type()) - .withBothComponent(TypePath::Property(name))); + results.push_back(isInvariantWith(env, it->second.type(), prop.type()).withBothComponent(TypePath::Property(name))); if (subTable->indexer) { @@ -1122,7 +1229,7 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const Tabl { return isInvariantWith(env, subIndexer.indexType, superIndexer.indexType) .withBothComponent(TypePath::TypeField::IndexLookup) - .andAlso(isInvariantWith(env, superIndexer.indexResultType, subIndexer.indexResultType).withBothComponent(TypePath::TypeField::IndexResult)); + .andAlso(isInvariantWith(env, subIndexer.indexResultType, superIndexer.indexResultType).withBothComponent(TypePath::TypeField::IndexResult)); } SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const NormalizedType* subNorm, const NormalizedType* superNorm) @@ -1249,12 +1356,11 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const Type { std::vector results; - size_t i = 0; for (TypeId subTy : subTypes) { results.emplace_back(); for (TypeId superTy : superTypes) - results.back().orElse(isCovariantWith(env, subTy, superTy).withBothComponent(TypePath::Index{i++})); + results.back().orElse(isCovariantWith(env, subTy, superTy)); } return SubtypingResult::all(results); diff --git a/Analysis/src/ToString.cpp b/Analysis/src/ToString.cpp index 4f7869d0..918da330 100644 --- a/Analysis/src/ToString.cpp +++ b/Analysis/src/ToString.cpp @@ -1721,7 +1721,7 @@ std::string toString(const Constraint& constraint, ToStringOptions& opts) std::string iteratorStr = tos(c.iterator); std::string variableStr = tos(c.variables); - return variableStr + " ~ Iterate<" + iteratorStr + ">"; + return variableStr + " ~ iterate " + iteratorStr; } else if constexpr (std::is_same_v) { diff --git a/Analysis/src/TypeChecker2.cpp b/Analysis/src/TypeChecker2.cpp index 0250817c..32b91637 100644 --- a/Analysis/src/TypeChecker2.cpp +++ b/Analysis/src/TypeChecker2.cpp @@ -2467,6 +2467,8 @@ struct TypeChecker2 std::string relation = "a subtype of"; if (reasoning.variance == SubtypingVariance::Invariant) relation = "exactly"; + else if (reasoning.variance == SubtypingVariance::Contravariant) + relation = "a supertype of"; std::string reason; if (reasoning.subPath == reasoning.superPath) diff --git a/Analysis/src/TypePath.cpp b/Analysis/src/TypePath.cpp index 9b470a2a..fb4d68cb 100644 --- a/Analysis/src/TypePath.cpp +++ b/Analysis/src/TypePath.cpp @@ -12,7 +12,6 @@ #include #include #include -#include LUAU_FASTFLAG(DebugLuauReadWriteProperties); @@ -252,8 +251,6 @@ struct TraversalState TypeOrPack current; NotNull builtinTypes; - - DenseHashSet seen{nullptr}; int steps = 0; void updateCurrent(TypeId ty) @@ -268,18 +265,6 @@ struct TraversalState current = follow(tp); } - bool haveCycle() - { - const void* currentPtr = ptr(current); - - if (seen.contains(currentPtr)) - return true; - else - seen.insert(currentPtr); - - return false; - } - bool tooLong() { return ++steps > DFInt::LuauTypePathMaximumTraverseSteps; @@ -287,7 +272,7 @@ struct TraversalState bool checkInvariants() { - return haveCycle() || tooLong(); + return tooLong(); } bool traverse(const TypePath::Property& property) @@ -313,18 +298,36 @@ struct TraversalState { prop = lookupClassProp(c, property.name); } - else if (auto m = getMetatable(*currentType, builtinTypes)) + // For a metatable type, the table takes priority; check that before + // falling through to the metatable entry below. + else if (auto m = get(*currentType)) { - // Weird: rather than use findMetatableEntry, which requires a lot - // of stuff that we don't have and don't want to pull in, we use the - // path traversal logic to grab __index and then re-enter the lookup - // logic there. - updateCurrent(*m); + TypeOrPack pinned = current; + updateCurrent(m->table); - if (!traverse(TypePath::Property{"__index"})) - return false; + if (traverse(property)) + return true; - return traverse(property); + // Restore the old current type if we didn't traverse the metatable + // successfully; we'll use the next branch to address this. + current = pinned; + } + + if (!prop) + { + if (auto m = getMetatable(*currentType, builtinTypes)) + { + // Weird: rather than use findMetatableEntry, which requires a lot + // of stuff that we don't have and don't want to pull in, we use the + // path traversal logic to grab __index and then re-enter the lookup + // logic there. + updateCurrent(*m); + + if (!traverse(TypePath::Property{"__index"})) + return false; + + return traverse(property); + } } if (prop) diff --git a/Analysis/src/Unifier2.cpp b/Analysis/src/Unifier2.cpp index 41a5afb0..6b213aea 100644 --- a/Analysis/src/Unifier2.cpp +++ b/Analysis/src/Unifier2.cpp @@ -113,7 +113,7 @@ bool Unifier2::unify(TypeId subTy, TypeId superTy) return argResult && retResult; } - auto subTable = get(subTy); + auto subTable = getMutable(subTy); auto superTable = get(superTy); if (subTable && superTable) { @@ -210,7 +210,7 @@ bool Unifier2::unify(TypeId subTy, const IntersectionType* superIntersection) return result; } -bool Unifier2::unify(const TableType* subTable, const TableType* superTable) +bool Unifier2::unify(TableType* subTable, const TableType* superTable) { bool result = true; @@ -256,6 +256,21 @@ bool Unifier2::unify(const TableType* subTable, const TableType* superTable) result &= unify(subTable->indexer->indexResultType, superTable->indexer->indexResultType); } + if (!subTable->indexer && subTable->state == TableState::Unsealed && superTable->indexer) + { + /* + * Unsealed tables are always created from literal table expressions. We + * can't be completely certain whether such a table has an indexer just + * by the content of the expression itself, so we need to be a bit more + * flexible here. + * + * If we are trying to reconcile an unsealed table with a table that has + * an indexer, we therefore conclude that the unsealed table has the + * same indexer. + */ + subTable->indexer = *superTable->indexer; + } + return result; } diff --git a/Ast/include/Luau/Ast.h b/Ast/include/Luau/Ast.h index ad5592f5..2abda788 100644 --- a/Ast/include/Luau/Ast.h +++ b/Ast/include/Luau/Ast.h @@ -3,6 +3,7 @@ #include "Luau/Location.h" +#include #include #include #include @@ -91,10 +92,21 @@ struct AstArray { return data; } + const T* end() const { return data + size; } + + std::reverse_iterator rbegin() const + { + return std::make_reverse_iterator(end()); + } + + std::reverse_iterator rend() const + { + return std::make_reverse_iterator(begin()); + } }; struct AstTypeList diff --git a/Ast/src/Parser.cpp b/Ast/src/Parser.cpp index 510e5f0e..e4b840b2 100644 --- a/Ast/src/Parser.cpp +++ b/Ast/src/Parser.cpp @@ -19,8 +19,6 @@ LUAU_FASTINTVARIABLE(LuauParseErrorLimit, 100) LUAU_FASTFLAGVARIABLE(LuauClipExtraHasEndProps, false) LUAU_FASTFLAG(LuauCheckedFunctionSyntax) -LUAU_FASTFLAGVARIABLE(LuauParseImpreciseNumber, false) - namespace Luau { @@ -2168,11 +2166,8 @@ static ConstantNumberParseResult parseInteger(double& result, const char* data, return base == 2 ? ConstantNumberParseResult::BinOverflow : ConstantNumberParseResult::HexOverflow; } - if (FFlag::LuauParseImpreciseNumber) - { - if (value >= (1ull << 53) && static_cast(result) != value) - return ConstantNumberParseResult::Imprecise; - } + if (value >= (1ull << 53) && static_cast(result) != value) + return ConstantNumberParseResult::Imprecise; return ConstantNumberParseResult::Ok; } @@ -2190,32 +2185,24 @@ static ConstantNumberParseResult parseDouble(double& result, const char* data) char* end = nullptr; double value = strtod(data, &end); - if (FFlag::LuauParseImpreciseNumber) + // trailing non-numeric characters + if (*end != 0) + return ConstantNumberParseResult::Malformed; + + result = value; + + // for linting, we detect integer constants that are parsed imprecisely + // since the check is expensive we only perform it when the number is larger than the precise integer range + if (value >= double(1ull << 53) && strspn(data, "0123456789") == strlen(data)) { - // trailing non-numeric characters - if (*end != 0) - return ConstantNumberParseResult::Malformed; + char repr[512]; + snprintf(repr, sizeof(repr), "%.0f", value); - result = value; - - // for linting, we detect integer constants that are parsed imprecisely - // since the check is expensive we only perform it when the number is larger than the precise integer range - if (value >= double(1ull << 53) && strspn(data, "0123456789") == strlen(data)) - { - char repr[512]; - snprintf(repr, sizeof(repr), "%.0f", value); - - if (strcmp(repr, data) != 0) - return ConstantNumberParseResult::Imprecise; - } - - return ConstantNumberParseResult::Ok; - } - else - { - result = value; - return *end == 0 ? ConstantNumberParseResult::Ok : ConstantNumberParseResult::Malformed; + if (strcmp(repr, data) != 0) + return ConstantNumberParseResult::Imprecise; } + + return ConstantNumberParseResult::Ok; } // simpleexp -> NUMBER | STRING | NIL | true | false | ... | constructor | FUNCTION body | primaryexp diff --git a/CLI/Compile.cpp b/CLI/Compile.cpp index bc1d5c60..95043271 100644 --- a/CLI/Compile.cpp +++ b/CLI/Compile.cpp @@ -37,13 +37,19 @@ enum class RecordStats { None, Total, - Split + File, + Function }; struct GlobalOptions { int optimizationLevel = 1; int debugLevel = 1; + + std::string vectorLib; + std::string vectorCtor; + std::string vectorType; + } globalOptions; static Luau::CompileOptions copts() @@ -52,6 +58,11 @@ static Luau::CompileOptions copts() result.optimizationLevel = globalOptions.optimizationLevel; result.debugLevel = globalOptions.debugLevel; + // globalOptions outlive the CompileOptions, so it's safe to use string data pointers here + result.vectorLib = globalOptions.vectorLib.c_str(); + result.vectorCtor = globalOptions.vectorCtor.c_str(); + result.vectorType = globalOptions.vectorType.c_str(); + return result; } @@ -159,11 +170,26 @@ struct CompileStats \"blockLinearizationStats\": {\ \"constPropInstructionCount\": %u, \ \"timeSeconds\": %f\ -}}", +}", lines, bytecode, bytecodeInstructionCount, codegen, readTime, miscTime, parseTime, compileTime, codegenTime, lowerStats.totalFunctions, lowerStats.skippedFunctions, lowerStats.spillsToSlot, lowerStats.spillsToRestore, lowerStats.maxSpillSlotsUsed, lowerStats.blocksPreOpt, lowerStats.blocksPostOpt, lowerStats.maxBlockInstructions, lowerStats.regAllocErrors, lowerStats.loweringErrors, lowerStats.blockLinearizationStats.constPropInstructionCount, lowerStats.blockLinearizationStats.timeSeconds); + if (lowerStats.collectFunctionStats) + { + fprintf(fp, ", \"functions\": ["); + auto functionCount = lowerStats.functions.size(); + for (size_t i = 0; i < functionCount; ++i) + { + const Luau::CodeGen::FunctionStats& fstat = lowerStats.functions[i]; + fprintf(fp, "{\"name\": \"%s\", \"line\": %d, \"bcodeCount\": %u, \"irCount\": %u, \"asmCount\": %u}", fstat.name.c_str(), fstat.line, + fstat.bcodeCount, fstat.irCount, fstat.asmCount); + if (i < functionCount - 1) + fprintf(fp, ", "); + } + fprintf(fp, "]"); + } + fprintf(fp, "}"); } CompileStats& operator+=(const CompileStats& that) @@ -321,7 +347,11 @@ static void displayHelp(const char* argv0) printf(" -g: compile with debug level n (default 1, n should be between 0 and 2).\n"); printf(" --target=: compile code for specific architecture (a64, x64, a64_nf, x64_ms).\n"); printf(" --timetrace: record compiler time tracing information into trace.json\n"); - printf(" --record-stats=