From 76f67e0733949bcdf0a83e81213e8cb8797ed632 Mon Sep 17 00:00:00 2001 From: vegorov-rbx <75688451+vegorov-rbx@users.noreply.github.com> Date: Fri, 28 Jul 2023 08:13:53 -0700 Subject: [PATCH] Sync to upstream/release/588 (#992) Type checker/autocomplete: * `Luau::autocomplete` no longer performs typechecking internally, make sure to run `Frontend::check` before performing autocomplete requests * Autocomplete string suggestions without "" are now only suggested inside the "" * Autocomplete suggestions now include `function (anonymous autofilled)` key with a full suggestion for the function expression (with arguments included) stored in `AutocompleteEntry::insertText` * `AutocompleteEntry::indexedWithSelf` is provided for function call suggestions made with `:` * Cyclic modules now see each other type exports as `any` to prevent memory use-after-free (similar to module return type) Runtime: * Updated inline/loop unroll cost model to better handle assignments (Fixes https://github.com/Roblox/luau/issues/978) * `math.noise` speed was improved by ~30% * `table.concat` speed was improved by ~5-7% * `tonumber` and `tostring` now have fastcall paths that execute ~1.5x and ~2.5x faster respectively (fixes #777) * Fixed crash in `luaL_typename` when index refers to a non-existing value * Fixed potential out of memory scenario when using `string.sub` or `string.char` in a loop * Fixed behavior of some fastcall builtins when called without arguments under -O2 to match original functions * Support for native code execution in VM is now enabled by default (note: native code still has to be generated explicitly) * `Codegen::compile` now accepts `CodeGen_OnlyNativeModules` flag. When set, only modules that have a `--!native` hot-comment at the top will be compiled to native code In our new typechecker: * Generic type packs are no longer considered to be variadic during unification * Timeout and cancellation now works in new solver * Fixed false positive errors around 'table' and 'function' type refinements * Table literals now use covariant unification rules. This is sound since literal has no type specified and has no aliases * Fixed issues with blocked types escaping the constraint solver * Fixed more places where error messages that should've been suppressed were still reported * Fixed errors when iterating over a top table type In our native code generation (jit): * 'DebugLuauAbortingChecks' flag is now supported on A64 * LOP_NEWCLOSURE has been translated to IR --- Analysis/include/Luau/Autocomplete.h | 7 + Analysis/include/Luau/Config.h | 2 +- .../include/Luau/ConstraintGraphBuilder.h | 3 +- Analysis/include/Luau/ConstraintSolver.h | 8 +- Analysis/include/Luau/Differ.h | 20 +- Analysis/include/Luau/Frontend.h | 14 +- Analysis/include/Luau/Normalize.h | 3 + Analysis/include/Luau/Symbol.h | 14 +- Analysis/include/Luau/TypeArena.h | 4 + Analysis/include/Luau/TypeCheckLimits.h | 41 + Analysis/include/Luau/TypeInfer.h | 19 +- Analysis/include/Luau/TypeUtils.h | 46 + Analysis/include/Luau/Unifier.h | 29 +- Analysis/src/Autocomplete.cpp | 249 ++++- Analysis/src/Config.cpp | 3 - Analysis/src/ConstraintGraphBuilder.cpp | 21 +- Analysis/src/ConstraintSolver.cpp | 75 +- Analysis/src/Differ.cpp | 278 +++++- Analysis/src/Frontend.cpp | 61 +- Analysis/src/Linter.cpp | 9 + Analysis/src/Normalize.cpp | 9 +- Analysis/src/Quantify.cpp | 1 - Analysis/src/Scope.cpp | 14 +- Analysis/src/Simplify.cpp | 6 + Analysis/src/Symbol.cpp | 14 + Analysis/src/TxnLog.cpp | 2 - Analysis/src/TypeChecker2.cpp | 250 +++-- Analysis/src/TypeInfer.cpp | 16 + Analysis/src/TypeUtils.cpp | 54 ++ Analysis/src/Unifier.cpp | 206 +++-- CLI/Repl.cpp | 3 - CodeGen/include/Luau/CodeGen.h | 8 +- CodeGen/include/Luau/IrData.h | 16 +- CodeGen/include/Luau/IrUtils.h | 3 + CodeGen/include/luacodegen.h | 2 +- CodeGen/src/CodeGen.cpp | 12 +- CodeGen/src/IrAnalysis.cpp | 7 +- CodeGen/src/IrBuilder.cpp | 2 +- CodeGen/src/IrDump.cpp | 14 +- CodeGen/src/IrLoweringA64.cpp | 54 +- CodeGen/src/IrLoweringX64.cpp | 62 +- CodeGen/src/IrTranslation.cpp | 56 ++ CodeGen/src/IrTranslation.h | 1 + CodeGen/src/IrUtils.cpp | 7 +- CodeGen/src/IrValueLocationTracking.cpp | 3 +- CodeGen/src/NativeState.cpp | 2 + CodeGen/src/NativeState.h | 2 + CodeGen/src/OptimizeConstProp.cpp | 8 +- Common/include/Luau/Bytecode.h | 12 + Compiler/src/Builtins.cpp | 54 +- Compiler/src/Builtins.h | 9 + Compiler/src/Compiler.cpp | 37 +- Compiler/src/CostModel.cpp | 42 +- Sources.cmake | 1 + VM/include/luaconf.h | 2 +- VM/src/laux.cpp | 2 +- VM/src/lbuiltins.cpp | 77 ++ VM/src/lfunc.cpp | 1 + VM/src/lgc.h | 4 +- VM/src/lmathlib.cpp | 113 ++- VM/src/lobject.h | 2 + VM/src/ltablib.cpp | 6 +- VM/src/lvmexecute.cpp | 11 +- bench/micro_tests/test_ToNumberString.lua | 22 + tests/Autocomplete.test.cpp | 567 +++++++++++- tests/Compiler.test.cpp | 17 + tests/Conformance.test.cpp | 32 + tests/ConstraintGraphBuilderFixture.cpp | 4 +- tests/CostModel.test.cpp | 27 +- tests/Differ.test.cpp | 875 ++++++------------ tests/Fixture.cpp | 13 +- tests/Fixture.h | 49 + tests/Frontend.test.cpp | 47 + tests/Linter.test.cpp | 8 +- tests/Simplify.test.cpp | 11 + tests/StringUtils.test.cpp | 2 +- tests/ToString.test.cpp | 1 + tests/TypeInfer.anyerror.test.cpp | 2 + tests/TypeInfer.classes.test.cpp | 2 + tests/TypeInfer.functions.test.cpp | 22 +- tests/TypeInfer.generics.test.cpp | 3 +- tests/TypeInfer.intersectionTypes.test.cpp | 1 + tests/TypeInfer.provisional.test.cpp | 10 +- tests/TypeInfer.refinements.test.cpp | 42 + tests/TypeInfer.singletons.test.cpp | 2 + tests/TypeInfer.tables.test.cpp | 91 +- tests/TypeInfer.test.cpp | 41 +- tests/TypeInfer.tryUnify.test.cpp | 5 +- tests/TypeInfer.typePacks.cpp | 2 + tests/TypeInfer.unionTypes.test.cpp | 2 + tests/TypeInfer.unknownnever.test.cpp | 2 + tests/conformance/basic.lua | 5 + tests/conformance/math.lua | 7 + tests/conformance/strings.lua | 6 + tests/main.cpp | 8 +- tools/faillist.txt | 7 +- 96 files changed, 3057 insertions(+), 991 deletions(-) create mode 100644 Analysis/include/Luau/TypeCheckLimits.h create mode 100644 bench/micro_tests/test_ToNumberString.lua diff --git a/Analysis/include/Luau/Autocomplete.h b/Analysis/include/Luau/Autocomplete.h index 61832577..bc709c7f 100644 --- a/Analysis/include/Luau/Autocomplete.h +++ b/Analysis/include/Luau/Autocomplete.h @@ -38,6 +38,7 @@ enum class AutocompleteEntryKind String, Type, Module, + GeneratedFunction, }; enum class ParenthesesRecommendation @@ -70,6 +71,10 @@ struct AutocompleteEntry std::optional documentationSymbol = std::nullopt; Tags tags; ParenthesesRecommendation parens = ParenthesesRecommendation::None; + std::optional insertText; + + // Only meaningful if kind is Property. + bool indexedWithSelf = false; }; using AutocompleteEntryMap = std::unordered_map; @@ -94,4 +99,6 @@ using StringCompletionCallback = AutocompleteResult autocomplete(Frontend& frontend, const ModuleName& moduleName, Position position, StringCompletionCallback callback); +constexpr char kGeneratedAnonymousFunctionEntryName[] = "function (anonymous autofilled)"; + } // namespace Luau diff --git a/Analysis/include/Luau/Config.h b/Analysis/include/Luau/Config.h index 8ba4ffa5..88c10554 100644 --- a/Analysis/include/Luau/Config.h +++ b/Analysis/include/Luau/Config.h @@ -19,7 +19,7 @@ struct Config { Config(); - Mode mode; + Mode mode = Mode::Nonstrict; ParseOptions parseOptions; diff --git a/Analysis/include/Luau/ConstraintGraphBuilder.h b/Analysis/include/Luau/ConstraintGraphBuilder.h index ababe0a3..eb1b1fed 100644 --- a/Analysis/include/Luau/ConstraintGraphBuilder.h +++ b/Analysis/include/Luau/ConstraintGraphBuilder.h @@ -94,12 +94,13 @@ struct ConstraintGraphBuilder ScopePtr globalScope; std::function prepareModuleScope; + std::vector requireCycles; DcrLogger* logger; ConstraintGraphBuilder(ModulePtr module, TypeArena* arena, NotNull moduleResolver, NotNull builtinTypes, NotNull ice, const ScopePtr& globalScope, std::function prepareModuleScope, - DcrLogger* logger, NotNull dfg); + DcrLogger* logger, NotNull dfg, std::vector requireCycles); /** * Fabricates a new free type belonging to a given scope. diff --git a/Analysis/include/Luau/ConstraintSolver.h b/Analysis/include/Luau/ConstraintSolver.h index b26d88c3..cba2cbb4 100644 --- a/Analysis/include/Luau/ConstraintSolver.h +++ b/Analysis/include/Luau/ConstraintSolver.h @@ -8,6 +8,7 @@ #include "Luau/Normalize.h" #include "Luau/ToString.h" #include "Luau/Type.h" +#include "Luau/TypeCheckLimits.h" #include "Luau/Variant.h" #include @@ -81,9 +82,11 @@ struct ConstraintSolver std::vector requireCycles; DcrLogger* logger; + TypeCheckLimits limits; explicit ConstraintSolver(NotNull normalizer, NotNull rootScope, std::vector> constraints, - ModuleName moduleName, NotNull moduleResolver, std::vector requireCycles, DcrLogger* logger); + ModuleName moduleName, NotNull moduleResolver, std::vector requireCycles, DcrLogger* logger, + TypeCheckLimits limits); // Randomize the order in which to dispatch constraints void randomize(unsigned seed); @@ -280,6 +283,9 @@ private: TypePackId anyifyModuleReturnTypePackGenerics(TypePackId tp); + void throwTimeLimitError(); + void throwUserCancelError(); + ToStringOptions opts; }; diff --git a/Analysis/include/Luau/Differ.h b/Analysis/include/Luau/Differ.h index da8b6468..60f555dc 100644 --- a/Analysis/include/Luau/Differ.h +++ b/Analysis/include/Luau/Differ.h @@ -1,9 +1,11 @@ // This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details #pragma once +#include "Luau/DenseHash.h" #include "Luau/Type.h" #include #include +#include namespace Luau { @@ -17,6 +19,7 @@ struct DiffPathNode FunctionReturn, Union, Intersection, + Negation, }; Kind kind; // non-null when TableProperty @@ -54,11 +57,15 @@ struct DiffPathNodeLeaf std::optional tableProperty; std::optional minLength; bool isVariadic; - DiffPathNodeLeaf(std::optional ty, std::optional tableProperty, std::optional minLength, bool isVariadic) + // TODO: Rename to anonymousIndex, for both union and Intersection + std::optional unionIndex; + DiffPathNodeLeaf( + std::optional ty, std::optional tableProperty, std::optional minLength, bool isVariadic, std::optional unionIndex) : ty(ty) , tableProperty(tableProperty) , minLength(minLength) , isVariadic(isVariadic) + , unionIndex(unionIndex) { } @@ -66,6 +73,8 @@ struct DiffPathNodeLeaf static DiffPathNodeLeaf detailsTableProperty(TypeId ty, Name tableProperty); + static DiffPathNodeLeaf detailsUnionIndex(TypeId ty, size_t index); + static DiffPathNodeLeaf detailsLength(int minLength, bool isVariadic); static DiffPathNodeLeaf nullopts(); @@ -82,11 +91,12 @@ struct DiffError enum Kind { Normal, - MissingProperty, + MissingTableProperty, + MissingUnionMember, + MissingIntersectionMember, + IncompatibleGeneric, LengthMismatchInFnArgs, LengthMismatchInFnRets, - LengthMismatchInUnion, - LengthMismatchInIntersection, }; Kind kind; @@ -141,6 +151,8 @@ struct DifferEnvironment { TypeId rootLeft; TypeId rootRight; + + DenseHashMap genericMatchedPairs; }; DifferResult diff(TypeId ty1, TypeId ty2); diff --git a/Analysis/include/Luau/Frontend.h b/Analysis/include/Luau/Frontend.h index 5804b7a8..5853eb32 100644 --- a/Analysis/include/Luau/Frontend.h +++ b/Analysis/include/Luau/Frontend.h @@ -6,6 +6,7 @@ #include "Luau/ModuleResolver.h" #include "Luau/RequireTracer.h" #include "Luau/Scope.h" +#include "Luau/TypeCheckLimits.h" #include "Luau/TypeInfer.h" #include "Luau/Variant.h" @@ -189,14 +190,6 @@ struct Frontend std::optional getCheckResult(const ModuleName& name, bool accumulateNested, bool forAutocomplete = false); private: - struct TypeCheckLimits - { - std::optional finishTime; - std::optional instantiationChildLimit; - std::optional unifierIterationLimit; - std::shared_ptr cancellationToken; - }; - ModulePtr check(const SourceModule& sourceModule, Mode mode, std::vector requireCycles, std::optional environmentScope, bool forAutocomplete, bool recordJsonLog, TypeCheckLimits typeCheckLimits); @@ -248,11 +241,12 @@ public: ModulePtr check(const SourceModule& sourceModule, const std::vector& requireCycles, NotNull builtinTypes, NotNull iceHandler, NotNull moduleResolver, NotNull fileResolver, - const ScopePtr& globalScope, std::function prepareModuleScope, FrontendOptions options); + const ScopePtr& globalScope, std::function prepareModuleScope, FrontendOptions options, + TypeCheckLimits limits); ModulePtr check(const SourceModule& sourceModule, const std::vector& requireCycles, NotNull builtinTypes, NotNull iceHandler, NotNull moduleResolver, NotNull fileResolver, const ScopePtr& globalScope, std::function prepareModuleScope, FrontendOptions options, - bool recordJsonLog); + TypeCheckLimits limits, bool recordJsonLog); } // namespace Luau diff --git a/Analysis/include/Luau/Normalize.h b/Analysis/include/Luau/Normalize.h index 75c07a7b..0f9352d1 100644 --- a/Analysis/include/Luau/Normalize.h +++ b/Analysis/include/Luau/Normalize.h @@ -277,6 +277,9 @@ struct NormalizedType /// Returns true if this type should result in error suppressing behavior. bool shouldSuppressErrors() const; + /// Returns true if this type contains the primitve top table type, `table`. + bool hasTopTable() const; + // Helpers that improve readability of the above (they just say if the component is present) bool hasTops() const; bool hasBooleans() const; diff --git a/Analysis/include/Luau/Symbol.h b/Analysis/include/Luau/Symbol.h index b47554e0..337e2a9f 100644 --- a/Analysis/include/Luau/Symbol.h +++ b/Analysis/include/Luau/Symbol.h @@ -6,8 +6,6 @@ #include -LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution) - namespace Luau { @@ -42,17 +40,7 @@ struct Symbol return local != nullptr || global.value != nullptr; } - bool operator==(const Symbol& rhs) const - { - if (local) - return local == rhs.local; - else if (global.value) - return rhs.global.value && global == rhs.global.value; // Subtlety: AstName::operator==(const char*) uses strcmp, not pointer identity. - else if (FFlag::DebugLuauDeferredConstraintResolution) - return !rhs.local && !rhs.global.value; // Reflexivity: we already know `this` Symbol is empty, so check that rhs is. - else - return false; - } + bool operator==(const Symbol& rhs) const; bool operator!=(const Symbol& rhs) const { diff --git a/Analysis/include/Luau/TypeArena.h b/Analysis/include/Luau/TypeArena.h index 0e69bb4a..5f831f18 100644 --- a/Analysis/include/Luau/TypeArena.h +++ b/Analysis/include/Luau/TypeArena.h @@ -9,12 +9,16 @@ namespace Luau { +struct Module; struct TypeArena { TypedAllocator types; TypedAllocator typePacks; + // Owning module, if any + Module* owningModule = nullptr; + void clear(); template diff --git a/Analysis/include/Luau/TypeCheckLimits.h b/Analysis/include/Luau/TypeCheckLimits.h new file mode 100644 index 00000000..9eabe0ff --- /dev/null +++ b/Analysis/include/Luau/TypeCheckLimits.h @@ -0,0 +1,41 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#pragma once + +#include "Luau/Cancellation.h" +#include "Luau/Error.h" + +#include +#include +#include + +namespace Luau +{ + +class TimeLimitError : public InternalCompilerError +{ +public: + explicit TimeLimitError(const std::string& moduleName) + : InternalCompilerError("Typeinfer failed to complete in allotted time", moduleName) + { + } +}; + +class UserCancelError : public InternalCompilerError +{ +public: + explicit UserCancelError(const std::string& moduleName) + : InternalCompilerError("Analysis has been cancelled by user", moduleName) + { + } +}; + +struct TypeCheckLimits +{ + std::optional finishTime; + std::optional instantiationChildLimit; + std::optional unifierIterationLimit; + + std::shared_ptr cancellationToken; +}; + +} // namespace Luau diff --git a/Analysis/include/Luau/TypeInfer.h b/Analysis/include/Luau/TypeInfer.h index 79ee60c4..9a44af49 100644 --- a/Analysis/include/Luau/TypeInfer.h +++ b/Analysis/include/Luau/TypeInfer.h @@ -10,6 +10,7 @@ #include "Luau/Symbol.h" #include "Luau/TxnLog.h" #include "Luau/Type.h" +#include "Luau/TypeCheckLimits.h" #include "Luau/TypePack.h" #include "Luau/TypeUtils.h" #include "Luau/Unifier.h" @@ -56,24 +57,6 @@ struct HashBoolNamePair size_t operator()(const std::pair& pair) const; }; -class TimeLimitError : public InternalCompilerError -{ -public: - explicit TimeLimitError(const std::string& moduleName) - : InternalCompilerError("Typeinfer failed to complete in allotted time", moduleName) - { - } -}; - -class UserCancelError : public InternalCompilerError -{ -public: - explicit UserCancelError(const std::string& moduleName) - : InternalCompilerError("Analysis has been cancelled by user", moduleName) - { - } -}; - struct GlobalTypes { GlobalTypes(NotNull builtinTypes); diff --git a/Analysis/include/Luau/TypeUtils.h b/Analysis/include/Luau/TypeUtils.h index 84916cd2..793415ee 100644 --- a/Analysis/include/Luau/TypeUtils.h +++ b/Analysis/include/Luau/TypeUtils.h @@ -14,6 +14,7 @@ namespace Luau struct TxnLog; struct TypeArena; +class Normalizer; enum class ValueContext { @@ -55,6 +56,51 @@ std::vector reduceUnion(const std::vector& types); */ TypeId stripNil(NotNull builtinTypes, TypeArena& arena, TypeId ty); +enum class ErrorSuppression +{ + Suppress, + DoNotSuppress, + NormalizationFailed +}; + +/** + * Normalizes the given type using the normalizer to determine if the type + * should suppress any errors that would be reported involving it. + * @param normalizer the normalizer to use + * @param ty the type to check for error suppression + * @returns an enum indicating whether or not to suppress the error or to signal a normalization failure + */ +ErrorSuppression shouldSuppressErrors(NotNull normalizer, TypeId ty); + +/** + * Flattens and normalizes the given typepack using the normalizer to determine if the type + * should suppress any errors that would be reported involving it. + * @param normalizer the normalizer to use + * @param tp the typepack to check for error suppression + * @returns an enum indicating whether or not to suppress the error or to signal a normalization failure + */ +ErrorSuppression shouldSuppressErrors(NotNull normalizer, TypePackId tp); + +/** + * Normalizes the two given type using the normalizer to determine if either type + * should suppress any errors that would be reported involving it. + * @param normalizer the normalizer to use + * @param ty1 the first type to check for error suppression + * @param ty2 the second type to check for error suppression + * @returns an enum indicating whether or not to suppress the error or to signal a normalization failure + */ +ErrorSuppression shouldSuppressErrors(NotNull normalizer, TypeId ty1, TypeId ty2); + +/** + * Flattens and normalizes the two given typepacks using the normalizer to determine if either type + * should suppress any errors that would be reported involving it. + * @param normalizer the normalizer to use + * @param tp1 the first typepack to check for error suppression + * @param tp2 the second typepack to check for error suppression + * @returns an enum indicating whether or not to suppress the error or to signal a normalization failure + */ +ErrorSuppression shouldSuppressErrors(NotNull normalizer, TypePackId tp1, TypePackId tp2); + template const T* get(std::optional ty) { diff --git a/Analysis/include/Luau/Unifier.h b/Analysis/include/Luau/Unifier.h index 7a6a2f76..f7c5c94c 100644 --- a/Analysis/include/Luau/Unifier.h +++ b/Analysis/include/Luau/Unifier.h @@ -43,6 +43,21 @@ struct Widen : Substitution TypePackId operator()(TypePackId ty); }; +/** + * Normally, when we unify table properties, we must do so invariantly, but we + * can introduce a special exception: If the table property in the subtype + * position arises from a literal expression, it is safe to instead perform a + * covariant check. + * + * This is very useful for typechecking cases where table literals (and trees of + * table literals) are passed directly to functions. + * + * In this case, we know that the property has no other name referring to it and + * so it is perfectly safe for the function to mutate the table any way it + * wishes. + */ +using LiteralProperties = DenseHashSet; + // TODO: Use this more widely. struct UnifierOptions { @@ -80,7 +95,7 @@ struct Unifier // Configure the Unifier to test for scope subsumption via embedded Scope // pointers rather than TypeLevels. - void enableScopeTests(); + void enableNewSolver(); // Test whether the two type vars unify. Never commits the result. ErrorVec canUnify(TypeId subTy, TypeId superTy); @@ -90,10 +105,10 @@ struct Unifier * Populate the vector errors with any type errors that may arise. * Populate the transaction log with the set of TypeIds that need to be reset to undo the unification attempt. */ - void tryUnify(TypeId subTy, TypeId superTy, bool isFunctionCall = false, bool isIntersection = false); + void tryUnify(TypeId subTy, TypeId superTy, bool isFunctionCall = false, bool isIntersection = false, const LiteralProperties* aliasableMap = nullptr); private: - void tryUnify_(TypeId subTy, TypeId superTy, bool isFunctionCall = false, bool isIntersection = false); + void tryUnify_(TypeId subTy, TypeId superTy, bool isFunctionCall = false, bool isIntersection = false, const LiteralProperties* aliasableMap = nullptr); void tryUnifyUnionWithType(TypeId subTy, const UnionType* uv, TypeId superTy); // Traverse the two types provided and block on any BlockedTypes we find. @@ -108,7 +123,7 @@ private: void tryUnifyPrimitives(TypeId subTy, TypeId superTy); void tryUnifySingletons(TypeId subTy, TypeId superTy); void tryUnifyFunctions(TypeId subTy, TypeId superTy, bool isFunctionCall = false); - void tryUnifyTables(TypeId subTy, TypeId superTy, bool isIntersection = false); + void tryUnifyTables(TypeId subTy, TypeId superTy, bool isIntersection = false, const LiteralProperties* aliasableMap = nullptr); void tryUnifyScalarShape(TypeId subTy, TypeId superTy, bool reversed); void tryUnifyWithMetatable(TypeId subTy, TypeId superTy, bool reversed); void tryUnifyWithClass(TypeId subTy, TypeId superTy, bool reversed); @@ -163,8 +178,10 @@ private: // Available after regular type pack unification errors std::optional firstPackErrorPos; - // If true, we use the scope hierarchy rather than TypeLevels - bool useScopes = false; + // If true, we do a bunch of small things differently to work better with + // the new type inference engine. Most notably, we use the Scope hierarchy + // directly rather than using TypeLevels. + bool useNewSolver = false; }; void promoteTypeLevels(TxnLog& log, const TypeArena* arena, TypeLevel minLevel, Scope* outerScope, bool useScope, TypePackId tp); diff --git a/Analysis/src/Autocomplete.cpp b/Analysis/src/Autocomplete.cpp index d67eda8d..cd3f4c6e 100644 --- a/Analysis/src/Autocomplete.cpp +++ b/Analysis/src/Autocomplete.cpp @@ -13,6 +13,10 @@ #include LUAU_FASTFLAG(DebugLuauReadWriteProperties) +LUAU_FASTFLAGVARIABLE(LuauDisableCompletionOutsideQuotes, false) +LUAU_FASTFLAGVARIABLE(LuauAnonymousAutofilled, false); +LUAU_FASTFLAGVARIABLE(LuauAutocompleteLastTypecheck, false) +LUAU_FASTFLAGVARIABLE(LuauAutocompleteHideSelfArg, false) static const std::unordered_set kStatementStartingKeywords = { "while", "if", "local", "repeat", "function", "do", "for", "return", "break", "continue", "type", "export"}; @@ -280,18 +284,38 @@ static void autocompleteProps(const Module& module, TypeArena* typeArena, NotNul ParenthesesRecommendation parens = indexType == PropIndexType::Key ? ParenthesesRecommendation::None : getParenRecommendation(type, nodes, typeCorrect); - result[name] = AutocompleteEntry{ - AutocompleteEntryKind::Property, - type, - prop.deprecated, - isWrongIndexer(type), - typeCorrect, - containingClass, - &prop, - prop.documentationSymbol, - {}, - parens, - }; + if (FFlag::LuauAutocompleteHideSelfArg) + { + result[name] = AutocompleteEntry{ + AutocompleteEntryKind::Property, + type, + prop.deprecated, + isWrongIndexer(type), + typeCorrect, + containingClass, + &prop, + prop.documentationSymbol, + {}, + parens, + {}, + indexType == PropIndexType::Colon + }; + } + else + { + result[name] = AutocompleteEntry{ + AutocompleteEntryKind::Property, + type, + prop.deprecated, + isWrongIndexer(type), + typeCorrect, + containingClass, + &prop, + prop.documentationSymbol, + {}, + parens + }; + } } } }; @@ -591,14 +615,14 @@ std::optional getLocalTypeInScopeAt(const Module& module, Position posit return {}; } -static std::optional tryGetTypeNameInScope(ScopePtr scope, TypeId ty) +template +static std::optional tryToStringDetailed(const ScopePtr& scope, T ty, bool functionTypeArguments) { - if (!canSuggestInferredType(scope, ty)) - return std::nullopt; - + LUAU_ASSERT(FFlag::LuauAnonymousAutofilled); ToStringOptions opts; opts.useLineBreaks = false; opts.hideTableKind = true; + opts.functionTypeArguments = functionTypeArguments; opts.scope = scope; ToStringResult name = toStringDetailed(ty, opts); @@ -608,6 +632,30 @@ static std::optional tryGetTypeNameInScope(ScopePtr scope, TypeId ty) return name.name; } +static std::optional tryGetTypeNameInScope(ScopePtr scope, TypeId ty, bool functionTypeArguments = false) +{ + if (!canSuggestInferredType(scope, ty)) + return std::nullopt; + + if (FFlag::LuauAnonymousAutofilled) + { + return tryToStringDetailed(scope, ty, functionTypeArguments); + } + else + { + ToStringOptions opts; + opts.useLineBreaks = false; + opts.hideTableKind = true; + opts.scope = scope; + ToStringResult name = toStringDetailed(ty, opts); + + if (name.error || name.invalid || name.cycle || name.truncated) + return std::nullopt; + + return name.name; + } +} + static bool tryAddTypeCorrectSuggestion(AutocompleteEntryMap& result, ScopePtr scope, AstType* topType, TypeId inferredType, Position position) { std::optional ty; @@ -1297,6 +1345,14 @@ static std::optional autocompleteStringParams(const Source return std::nullopt; } + if (FFlag::LuauDisableCompletionOutsideQuotes && !nodes.back()->is()) + { + if (nodes.back()->location.end == position || nodes.back()->location.begin == position) + { + return std::nullopt; + } + } + AstExprCall* candidate = nodes.at(nodes.size() - 2)->as(); if (!candidate) { @@ -1361,6 +1417,140 @@ static AutocompleteResult autocompleteWhileLoopKeywords(std::vector an return {std::move(ret), std::move(ancestry), AutocompleteContext::Keyword}; } +static std::string makeAnonymous(const ScopePtr& scope, const FunctionType& funcTy) +{ + LUAU_ASSERT(FFlag::LuauAnonymousAutofilled); + std::string result = "function("; + + auto [args, tail] = Luau::flatten(funcTy.argTypes); + + bool first = true; + // Skip the implicit 'self' argument if call is indexed with ':' + for (size_t argIdx = 0; argIdx < args.size(); ++argIdx) + { + if (!first) + result += ", "; + else + first = false; + + std::string name; + if (argIdx < funcTy.argNames.size() && funcTy.argNames[argIdx]) + name = funcTy.argNames[argIdx]->name; + else + name = "a" + std::to_string(argIdx); + + if (std::optional type = tryGetTypeNameInScope(scope, args[argIdx], true)) + result += name + ": " + *type; + else + result += name; + } + + if (tail && (Luau::isVariadic(*tail) || Luau::get(Luau::follow(*tail)))) + { + if (!first) + result += ", "; + + std::optional varArgType; + if (const VariadicTypePack* pack = get(follow(*tail))) + { + if (std::optional res = tryToStringDetailed(scope, pack->ty, true)) + varArgType = std::move(res); + } + + if (varArgType) + result += "...: " + *varArgType; + else + result += "..."; + } + + result += ")"; + + auto [rets, retTail] = Luau::flatten(funcTy.retTypes); + if (const size_t totalRetSize = rets.size() + (retTail ? 1 : 0); totalRetSize > 0) + { + if (std::optional returnTypes = tryToStringDetailed(scope, funcTy.retTypes, true)) + { + result += ": "; + bool wrap = totalRetSize != 1; + if (wrap) + result += "("; + result += *returnTypes; + if (wrap) + result += ")"; + } + } + result += " end"; + return result; +} + +static std::optional makeAnonymousAutofilled(const ModulePtr& module, Position position, const AstNode* node, const std::vector& ancestry) +{ + LUAU_ASSERT(FFlag::LuauAnonymousAutofilled); + const AstExprCall* call = node->as(); + if (!call && ancestry.size() > 1) + call = ancestry[ancestry.size() - 2]->as(); + + if (!call) + return std::nullopt; + + if (!call->location.containsClosed(position) || call->func->location.containsClosed(position)) + return std::nullopt; + + TypeId* typeIter = module->astTypes.find(call->func); + if (!typeIter) + return std::nullopt; + + const FunctionType* outerFunction = get(follow(*typeIter)); + if (!outerFunction) + return std::nullopt; + + size_t argument = 0; + for (size_t i = 0; i < call->args.size; ++i) + { + if (call->args.data[i]->location.containsClosed(position)) + { + argument = i; + break; + } + } + + if (call->self) + argument++; + + std::optional argType; + auto [args, tail] = flatten(outerFunction->argTypes); + if (argument < args.size()) + argType = args[argument]; + + if (!argType) + return std::nullopt; + + TypeId followed = follow(*argType); + const FunctionType* type = get(followed); + if (!type) + { + if (const UnionType* unionType = get(followed)) + { + if (std::optional nonnullFunction = returnFirstNonnullOptionOfType(unionType)) + type = *nonnullFunction; + } + } + + if (!type) + return std::nullopt; + + const ScopePtr scope = findScopeAtPosition(*module, position); + if (!scope) + return std::nullopt; + + AutocompleteEntry entry; + entry.kind = AutocompleteEntryKind::GeneratedFunction; + entry.typeCorrect = TypeCorrectKind::Correct; + entry.type = argType; + entry.insertText = makeAnonymous(scope, *type); + return std::make_optional(std::move(entry)); +} + static AutocompleteResult autocomplete(const SourceModule& sourceModule, const ModulePtr& module, NotNull builtinTypes, TypeArena* typeArena, Scope* globalScope, Position position, StringCompletionCallback callback) { @@ -1612,7 +1802,19 @@ static AutocompleteResult autocomplete(const SourceModule& sourceModule, const M return {}; if (node->asExpr()) - return autocompleteExpression(sourceModule, *module, builtinTypes, typeArena, ancestry, position); + { + if (FFlag::LuauAnonymousAutofilled) + { + AutocompleteResult ret = autocompleteExpression(sourceModule, *module, builtinTypes, typeArena, ancestry, position); + if (std::optional generated = makeAnonymousAutofilled(module, position, node, ancestry)) + ret.entryMap[kGeneratedAnonymousFunctionEntryName] = std::move(*generated); + return ret; + } + else + { + return autocompleteExpression(sourceModule, *module, builtinTypes, typeArena, ancestry, position); + } + } else if (node->asStat()) return {autocompleteStatement(sourceModule, *module, ancestry, position), ancestry, AutocompleteContext::Statement}; @@ -1621,11 +1823,14 @@ static AutocompleteResult autocomplete(const SourceModule& sourceModule, const M AutocompleteResult autocomplete(Frontend& frontend, const ModuleName& moduleName, Position position, StringCompletionCallback callback) { - // FIXME: We can improve performance here by parsing without checking. - // The old type graph is probably fine. (famous last words!) - FrontendOptions opts; - opts.forAutocomplete = true; - frontend.check(moduleName, opts); + if (!FFlag::LuauAutocompleteLastTypecheck) + { + // FIXME: We can improve performance here by parsing without checking. + // The old type graph is probably fine. (famous last words!) + FrontendOptions opts; + opts.forAutocomplete = true; + frontend.check(moduleName, opts); + } const SourceModule* sourceModule = frontend.getSourceModule(moduleName); if (!sourceModule) diff --git a/Analysis/src/Config.cpp b/Analysis/src/Config.cpp index 00ca7b16..9369743e 100644 --- a/Analysis/src/Config.cpp +++ b/Analysis/src/Config.cpp @@ -4,15 +4,12 @@ #include "Luau/Lexer.h" #include "Luau/StringUtils.h" -LUAU_FASTFLAGVARIABLE(LuauEnableNonstrictByDefaultForLuauConfig, false) - namespace Luau { using Error = std::optional; Config::Config() - : mode(FFlag::LuauEnableNonstrictByDefaultForLuauConfig ? Mode::Nonstrict : Mode::NoCheck) { enabledLint.setDefaults(); } diff --git a/Analysis/src/ConstraintGraphBuilder.cpp b/Analysis/src/ConstraintGraphBuilder.cpp index c62c214c..9c2766ec 100644 --- a/Analysis/src/ConstraintGraphBuilder.cpp +++ b/Analysis/src/ConstraintGraphBuilder.cpp @@ -139,7 +139,8 @@ void forEachConstraint(const Checkpoint& start, const Checkpoint& end, const Con ConstraintGraphBuilder::ConstraintGraphBuilder(ModulePtr module, TypeArena* arena, NotNull moduleResolver, NotNull builtinTypes, NotNull ice, const ScopePtr& globalScope, - std::function prepareModuleScope, DcrLogger* logger, NotNull dfg) + std::function prepareModuleScope, DcrLogger* logger, NotNull dfg, + std::vector requireCycles) : module(module) , builtinTypes(builtinTypes) , arena(arena) @@ -149,6 +150,7 @@ ConstraintGraphBuilder::ConstraintGraphBuilder(ModulePtr module, TypeArena* aren , ice(ice) , globalScope(globalScope) , prepareModuleScope(std::move(prepareModuleScope)) + , requireCycles(std::move(requireCycles)) , logger(logger) { LUAU_ASSERT(module); @@ -703,6 +705,16 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatLocal* l { scope->importedTypeBindings[name] = module->exportedTypeBindings; scope->importedModules[name] = moduleInfo->name; + + // Imported types of requires that transitively refer to current module have to be replaced with 'any' + for (const auto& [location, path] : requireCycles) + { + if (!path.empty() && path.front() == moduleInfo->name) + { + for (auto& [name, tf] : scope->importedTypeBindings[name]) + tf = TypeFun{{}, {}, builtinTypes->anyType}; + } + } } } } @@ -1913,6 +1925,9 @@ std::tuple ConstraintGraphBuilder::checkBinary( NullableBreadcrumbId bc = dfg->getBreadcrumb(typeguard->target); if (!bc) return {leftType, rightType, nullptr}; + auto augmentForErrorSupression = [&](TypeId ty) -> TypeId { + return arena->addType(UnionType{{ty, builtinTypes->errorType}}); + }; TypeId discriminantTy = builtinTypes->neverType; if (typeguard->type == "nil") @@ -1926,9 +1941,9 @@ std::tuple ConstraintGraphBuilder::checkBinary( else if (typeguard->type == "thread") discriminantTy = builtinTypes->threadType; else if (typeguard->type == "table") - discriminantTy = builtinTypes->tableType; + discriminantTy = augmentForErrorSupression(builtinTypes->tableType); else if (typeguard->type == "function") - discriminantTy = builtinTypes->functionType; + discriminantTy = augmentForErrorSupression(builtinTypes->functionType); else if (typeguard->type == "userdata") { // For now, we don't really care about being accurate with userdata if the typeguard was using typeof. diff --git a/Analysis/src/ConstraintSolver.cpp b/Analysis/src/ConstraintSolver.cpp index c9b584fd..fbe08162 100644 --- a/Analysis/src/ConstraintSolver.cpp +++ b/Analysis/src/ConstraintSolver.cpp @@ -12,6 +12,7 @@ #include "Luau/ModuleResolver.h" #include "Luau/Quantify.h" #include "Luau/Simplify.h" +#include "Luau/TimeTrace.h" #include "Luau/ToString.h" #include "Luau/Type.h" #include "Luau/TypeFamily.h" @@ -259,7 +260,7 @@ struct InstantiationQueuer : TypeOnceVisitor }; ConstraintSolver::ConstraintSolver(NotNull normalizer, NotNull rootScope, std::vector> constraints, - ModuleName moduleName, NotNull moduleResolver, std::vector requireCycles, DcrLogger* logger) + ModuleName moduleName, NotNull moduleResolver, std::vector requireCycles, DcrLogger* logger, TypeCheckLimits limits) : arena(normalizer->arena) , builtinTypes(normalizer->builtinTypes) , normalizer(normalizer) @@ -269,6 +270,7 @@ ConstraintSolver::ConstraintSolver(NotNull normalizer, NotNull *limits.finishTime) + throwTimeLimitError(); + if (limits.cancellationToken && limits.cancellationToken->requested()) + throwUserCancelError(); + std::string saveMe = FFlag::DebugLuauLogSolver ? toString(*c, opts) : std::string{}; StepSnapshot snapshot; @@ -555,6 +562,9 @@ bool ConstraintSolver::tryDispatch(const InstantiationConstraint& c, NotNullscope); + if (limits.instantiationChildLimit) + inst.childLimit = *limits.instantiationChildLimit; + std::optional instantiated = inst.substitute(c.superType); LUAU_ASSERT(get(c.subType)); @@ -586,7 +596,7 @@ bool ConstraintSolver::tryDispatch(const UnaryConstraint& c, NotNull(operandType)) + if (!force && get(operandType)) return block(operandType, constraint); LUAU_ASSERT(get(c.resultType)); @@ -713,6 +723,10 @@ bool ConstraintSolver::tryDispatch(const BinaryConstraint& c, NotNullscope}; + + if (limits.instantiationChildLimit) + instantiation.childLimit = *limits.instantiationChildLimit; + std::optional instantiatedMm = instantiation.substitute(*mm); if (!instantiatedMm) { @@ -1318,6 +1332,9 @@ bool ConstraintSolver::tryDispatch(const FunctionCallConstraint& c, NotNullscope); + if (limits.instantiationChildLimit) + inst.childLimit = *limits.instantiationChildLimit; + std::vector arityMatchingOverloads; std::optional bestOverloadLog; @@ -1334,7 +1351,7 @@ bool ConstraintSolver::tryDispatch(const FunctionCallConstraint& c, NotNullscope, Location{}, Covariant}; - u.enableScopeTests(); + u.enableNewSolver(); u.tryUnify(*instantiated, inferredTy, /* isFunctionCall */ true); @@ -1384,7 +1401,7 @@ bool ConstraintSolver::tryDispatch(const FunctionCallConstraint& c, NotNullscope, Location{}, Covariant}; - u.enableScopeTests(); + u.enableNewSolver(); u.tryUnify(inferredTy, builtinTypes->anyType); u.tryUnify(fn, builtinTypes->anyType); @@ -1505,6 +1522,7 @@ static void updateTheTableType( for (size_t i = 0; i < path.size() - 1; ++i) { + t = follow(t); auto propTy = findTablePropertyRespectingMeta(builtinTypes, dummy, t, path[i], Location{}); dummy.clear(); @@ -1885,19 +1903,20 @@ bool ConstraintSolver::tryDispatch(const RefineConstraint& c, NotNullnormalize(c.type); - - if (!normType) - reportError(NormalizationTooComplex{}, constraint->location); - - if (normType && normType->shouldSuppressErrors()) + switch (shouldSuppressErrors(normalizer, c.type)) + { + case ErrorSuppression::Suppress: { auto resultOrError = simplifyUnion(builtinTypes, arena, result, builtinTypes->errorType).result; asMutable(c.resultType)->ty.emplace(resultOrError); + break; } - else - { + case ErrorSuppression::DoNotSuppress: asMutable(c.resultType)->ty.emplace(result); + break; + case ErrorSuppression::NormalizationFailed: + reportError(NormalizationTooComplex{}, constraint->location); + break; } unblock(c.resultType, constraint->location); @@ -1983,6 +2002,15 @@ bool ConstraintSolver::tryDispatchIterableTable(TypeId iteratorTy, const Iterabl unify(*anyified, ty, constraint->scope); }; + auto unknownify = [&](auto ty) { + Anyification anyify{arena, constraint->scope, builtinTypes, &iceReporter, builtinTypes->unknownType, builtinTypes->anyTypePack}; + std::optional anyified = anyify.substitute(ty); + if (!anyified) + reportError(CodeTooComplex{}, constraint->location); + else + unify(*anyified, ty, constraint->scope); + }; + auto errorify = [&](auto ty) { Anyification anyify{arena, constraint->scope, builtinTypes, &iceReporter, errorRecoveryType(), errorRecoveryTypePack()}; std::optional errorified = anyify.substitute(ty); @@ -2051,6 +2079,9 @@ bool ConstraintSolver::tryDispatchIterableTable(TypeId iteratorTy, const Iterabl Instantiation instantiation(TxnLog::empty(), arena, TypeLevel{}, constraint->scope); + if (limits.instantiationChildLimit) + instantiation.childLimit = *limits.instantiationChildLimit; + if (std::optional instantiatedIterFn = instantiation.substitute(*iterFn)) { if (auto iterFtv = get(*instantiatedIterFn)) @@ -2107,6 +2138,8 @@ bool ConstraintSolver::tryDispatchIterableTable(TypeId iteratorTy, const Iterabl LUAU_ASSERT(false); } + else if (auto primitiveTy = get(iteratorTy); primitiveTy && primitiveTy->type == PrimitiveType::Type::Table) + unknownify(c.variables); else errorify(c.variables); @@ -2357,7 +2390,7 @@ template bool ConstraintSolver::tryUnify(NotNull constraint, TID subTy, TID superTy) { Unifier u{normalizer, constraint->scope, constraint->location, Covariant}; - u.enableScopeTests(); + u.enableNewSolver(); u.tryUnify(subTy, superTy); @@ -2606,7 +2639,7 @@ bool ConstraintSolver::isBlocked(NotNull constraint) ErrorVec ConstraintSolver::unify(TypeId subType, TypeId superType, NotNull scope) { Unifier u{normalizer, scope, Location{}, Covariant}; - u.enableScopeTests(); + u.enableNewSolver(); u.tryUnify(subType, superType); @@ -2631,7 +2664,7 @@ ErrorVec ConstraintSolver::unify(TypePackId subPack, TypePackId superPack, NotNu { UnifierSharedState sharedState{&iceReporter}; Unifier u{normalizer, scope, Location{}, Covariant}; - u.enableScopeTests(); + u.enableNewSolver(); u.tryUnify(subPack, superPack); @@ -2728,7 +2761,7 @@ TypeId ConstraintSolver::unionOfTypes(TypeId a, TypeId b, NotNull scope, if (unifyFreeTypes && (get(a) || get(b))) { Unifier u{normalizer, scope, Location{}, Covariant}; - u.enableScopeTests(); + u.enableNewSolver(); u.tryUnify(b, a); if (u.errors.empty()) @@ -2785,4 +2818,14 @@ TypePackId ConstraintSolver::anyifyModuleReturnTypePackGenerics(TypePackId tp) return arena->addTypePack(resultTypes, resultTail); } +LUAU_NOINLINE void ConstraintSolver::throwTimeLimitError() +{ + throw TimeLimitError(currentModuleName); +} + +LUAU_NOINLINE void ConstraintSolver::throwUserCancelError() +{ + throw UserCancelError(currentModuleName); +} + } // namespace Luau diff --git a/Analysis/src/Differ.cpp b/Analysis/src/Differ.cpp index 50672cd9..307446ef 100644 --- a/Analysis/src/Differ.cpp +++ b/Analysis/src/Differ.cpp @@ -1,11 +1,15 @@ // This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details #include "Luau/Differ.h" +#include "Luau/Common.h" #include "Luau/Error.h" #include "Luau/ToString.h" #include "Luau/Type.h" #include "Luau/TypePack.h" +#include "Luau/Unifiable.h" #include #include +#include +#include namespace Luau { @@ -34,6 +38,10 @@ std::string DiffPathNode::toString() const // Add 1 because Lua is 1-indexed return "Ret[" + std::to_string(*index + 1) + "]"; } + case DiffPathNode::Kind::Negation: + { + return "Negation"; + } default: { throw InternalCompilerError{"DiffPathNode::toString is not exhaustive"}; @@ -58,22 +66,27 @@ DiffPathNode DiffPathNode::constructWithKind(Kind kind) DiffPathNodeLeaf DiffPathNodeLeaf::detailsNormal(TypeId ty) { - return DiffPathNodeLeaf{ty, std::nullopt, std::nullopt, false}; + return DiffPathNodeLeaf{ty, std::nullopt, std::nullopt, false, std::nullopt}; } DiffPathNodeLeaf DiffPathNodeLeaf::detailsTableProperty(TypeId ty, Name tableProperty) { - return DiffPathNodeLeaf{ty, tableProperty, std::nullopt, false}; + return DiffPathNodeLeaf{ty, tableProperty, std::nullopt, false, std::nullopt}; +} + +DiffPathNodeLeaf DiffPathNodeLeaf::detailsUnionIndex(TypeId ty, size_t index) +{ + return DiffPathNodeLeaf{ty, std::nullopt, std::nullopt, false, index}; } DiffPathNodeLeaf DiffPathNodeLeaf::detailsLength(int minLength, bool isVariadic) { - return DiffPathNodeLeaf{std::nullopt, std::nullopt, minLength, isVariadic}; + return DiffPathNodeLeaf{std::nullopt, std::nullopt, minLength, isVariadic, std::nullopt}; } DiffPathNodeLeaf DiffPathNodeLeaf::nullopts() { - return DiffPathNodeLeaf{std::nullopt, std::nullopt, std::nullopt, false}; + return DiffPathNodeLeaf{std::nullopt, std::nullopt, std::nullopt, false, std::nullopt}; } std::string DiffPath::toString(bool prependDot) const @@ -104,7 +117,7 @@ std::string DiffError::toStringALeaf(std::string rootName, const DiffPathNodeLea checkNonMissingPropertyLeavesHaveNulloptTableProperty(); return pathStr + " has type " + Luau::toString(*leaf.ty); } - case DiffError::Kind::MissingProperty: + case DiffError::Kind::MissingTableProperty: { if (leaf.ty.has_value()) { @@ -120,6 +133,38 @@ std::string DiffError::toStringALeaf(std::string rootName, const DiffPathNodeLea } throw InternalCompilerError{"Both leaf.ty and otherLeaf.ty is nullopt"}; } + case DiffError::Kind::MissingUnionMember: + { + // TODO: do normal case + if (leaf.ty.has_value()) + { + if (!leaf.unionIndex.has_value()) + throw InternalCompilerError{"leaf.unionIndex is nullopt"}; + return pathStr + " is a union containing type " + Luau::toString(*leaf.ty); + } + else if (otherLeaf.ty.has_value()) + { + return pathStr + " is a union missing type " + Luau::toString(*otherLeaf.ty); + } + throw InternalCompilerError{"Both leaf.ty and otherLeaf.ty is nullopt"}; + } + case DiffError::Kind::MissingIntersectionMember: + { + // TODO: better message for intersections + // An intersection of just functions is always an "overloaded function" + // An intersection of just tables is always a "joined table" + if (leaf.ty.has_value()) + { + if (!leaf.unionIndex.has_value()) + throw InternalCompilerError{"leaf.unionIndex is nullopt"}; + return pathStr + " is an intersection containing type " + Luau::toString(*leaf.ty); + } + else if (otherLeaf.ty.has_value()) + { + return pathStr + " is an intersection missing type " + Luau::toString(*otherLeaf.ty); + } + throw InternalCompilerError{"Both leaf.ty and otherLeaf.ty is nullopt"}; + } case DiffError::Kind::LengthMismatchInFnArgs: { if (!leaf.minLength.has_value()) @@ -163,9 +208,20 @@ std::string getDevFixFriendlyName(TypeId ty) std::string DiffError::toString() const { - std::string msg = "DiffError: these two types are not equal because the left type at " + toStringALeaf(leftRootName, left, right) + - ", while the right type at " + toStringALeaf(rightRootName, right, left); - return msg; + switch (kind) + { + case DiffError::Kind::IncompatibleGeneric: + { + std::string diffPathStr{diffPath.toString(true)}; + return "DiffError: these two types are not equal because the left generic at " + leftRootName + diffPathStr + + " cannot be the same type parameter as the right generic at " + rightRootName + diffPathStr; + } + default: + { + return "DiffError: these two types are not equal because the left type at " + toStringALeaf(leftRootName, left, right) + + ", while the right type at " + toStringALeaf(rightRootName, right, left); + } + } } void DiffError::checkValidInitialization(const DiffPathNodeLeaf& left, const DiffPathNodeLeaf& right) @@ -193,6 +249,19 @@ static DifferResult diffTable(DifferEnvironment& env, TypeId left, TypeId right) static DifferResult diffPrimitive(DifferEnvironment& env, TypeId left, TypeId right); static DifferResult diffSingleton(DifferEnvironment& env, TypeId left, TypeId right); static DifferResult diffFunction(DifferEnvironment& env, TypeId left, TypeId right); +static DifferResult diffGeneric(DifferEnvironment& env, TypeId left, TypeId right); +static DifferResult diffNegation(DifferEnvironment& env, TypeId left, TypeId right); +struct FindSeteqCounterexampleResult +{ + // nullopt if no counterexample found + std::optional mismatchIdx; + // true if counterexample is in the left, false if cex is in the right + bool inLeft; +}; +static FindSeteqCounterexampleResult findSeteqCounterexample( + DifferEnvironment& env, const std::vector& left, const std::vector& right); +static DifferResult diffUnion(DifferEnvironment& env, TypeId left, TypeId right); +static DifferResult diffIntersection(DifferEnvironment& env, TypeId left, TypeId right); /** * The last argument gives context info on which complex type contained the TypePack. */ @@ -205,6 +274,8 @@ static DifferResult diffTable(DifferEnvironment& env, TypeId left, TypeId right) { const TableType* leftTable = get(left); const TableType* rightTable = get(right); + LUAU_ASSERT(leftTable); + LUAU_ASSERT(rightTable); for (auto const& [field, value] : leftTable->props) { @@ -212,7 +283,7 @@ static DifferResult diffTable(DifferEnvironment& env, TypeId left, TypeId right) { // left has a field the right doesn't return DifferResult{DiffError{ - DiffError::Kind::MissingProperty, + DiffError::Kind::MissingTableProperty, DiffPathNodeLeaf::detailsTableProperty(value.type(), field), DiffPathNodeLeaf::nullopts(), getDevFixFriendlyName(env.rootLeft), @@ -225,9 +296,9 @@ static DifferResult diffTable(DifferEnvironment& env, TypeId left, TypeId right) if (leftTable->props.find(field) == leftTable->props.end()) { // right has a field the left doesn't - return DifferResult{ - DiffError{DiffError::Kind::MissingProperty, DiffPathNodeLeaf::nullopts(), DiffPathNodeLeaf::detailsTableProperty(value.type(), field), - getDevFixFriendlyName(env.rootLeft), getDevFixFriendlyName(env.rootRight)}}; + return DifferResult{DiffError{DiffError::Kind::MissingTableProperty, DiffPathNodeLeaf::nullopts(), + DiffPathNodeLeaf::detailsTableProperty(value.type(), field), getDevFixFriendlyName(env.rootLeft), + getDevFixFriendlyName(env.rootRight)}}; } } // left and right have the same set of keys @@ -248,6 +319,8 @@ static DifferResult diffPrimitive(DifferEnvironment& env, TypeId left, TypeId ri { const PrimitiveType* leftPrimitive = get(left); const PrimitiveType* rightPrimitive = get(right); + LUAU_ASSERT(leftPrimitive); + LUAU_ASSERT(rightPrimitive); if (leftPrimitive->type != rightPrimitive->type) { @@ -266,6 +339,8 @@ static DifferResult diffSingleton(DifferEnvironment& env, TypeId left, TypeId ri { const SingletonType* leftSingleton = get(left); const SingletonType* rightSingleton = get(right); + LUAU_ASSERT(leftSingleton); + LUAU_ASSERT(rightSingleton); if (*leftSingleton != *rightSingleton) { @@ -284,6 +359,8 @@ static DifferResult diffFunction(DifferEnvironment& env, TypeId left, TypeId rig { const FunctionType* leftFunction = get(left); const FunctionType* rightFunction = get(right); + LUAU_ASSERT(leftFunction); + LUAU_ASSERT(rightFunction); DifferResult differResult = diffTpi(env, DiffError::Kind::LengthMismatchInFnArgs, leftFunction->argTypes, rightFunction->argTypes); if (differResult.diffError.has_value()) @@ -291,6 +368,157 @@ static DifferResult diffFunction(DifferEnvironment& env, TypeId left, TypeId rig return diffTpi(env, DiffError::Kind::LengthMismatchInFnRets, leftFunction->retTypes, rightFunction->retTypes); } +static DifferResult diffGeneric(DifferEnvironment& env, TypeId left, TypeId right) +{ + LUAU_ASSERT(get(left)); + LUAU_ASSERT(get(right)); + // Try to pair up the generics + bool isLeftFree = !env.genericMatchedPairs.contains(left); + bool isRightFree = !env.genericMatchedPairs.contains(right); + if (isLeftFree && isRightFree) + { + env.genericMatchedPairs[left] = right; + env.genericMatchedPairs[right] = left; + return DifferResult{}; + } + else if (isLeftFree || isRightFree) + { + return DifferResult{DiffError{ + DiffError::Kind::IncompatibleGeneric, + DiffPathNodeLeaf::nullopts(), + DiffPathNodeLeaf::nullopts(), + getDevFixFriendlyName(env.rootLeft), + getDevFixFriendlyName(env.rootRight), + }}; + } + + // Both generics are already paired up + if (*env.genericMatchedPairs.find(left) == right) + return DifferResult{}; + + return DifferResult{DiffError{ + DiffError::Kind::IncompatibleGeneric, + DiffPathNodeLeaf::nullopts(), + DiffPathNodeLeaf::nullopts(), + getDevFixFriendlyName(env.rootLeft), + getDevFixFriendlyName(env.rootRight), + }}; +} + +static DifferResult diffNegation(DifferEnvironment& env, TypeId left, TypeId right) +{ + const NegationType* leftNegation = get(left); + const NegationType* rightNegation = get(right); + LUAU_ASSERT(leftNegation); + LUAU_ASSERT(rightNegation); + + DifferResult differResult = diffUsingEnv(env, leftNegation->ty, rightNegation->ty); + if (!differResult.diffError.has_value()) + return DifferResult{}; + + differResult.wrapDiffPath(DiffPathNode::constructWithKind(DiffPathNode::Kind::Negation)); + return differResult; +} + +static FindSeteqCounterexampleResult findSeteqCounterexample( + DifferEnvironment& env, const std::vector& left, const std::vector& right) +{ + std::unordered_set unmatchedRightIdxes; + for (size_t i = 0; i < right.size(); i++) + unmatchedRightIdxes.insert(i); + for (size_t leftIdx = 0; leftIdx < left.size(); leftIdx++) + { + bool leftIdxIsMatched = false; + auto unmatchedRightIdxIt = unmatchedRightIdxes.begin(); + while (unmatchedRightIdxIt != unmatchedRightIdxes.end()) + { + DifferResult differResult = diffUsingEnv(env, left[leftIdx], right[*unmatchedRightIdxIt]); + if (differResult.diffError.has_value()) + { + unmatchedRightIdxIt++; + continue; + } + + // unmatchedRightIdxIt is matched with current leftIdx + leftIdxIsMatched = true; + unmatchedRightIdxIt = unmatchedRightIdxes.erase(unmatchedRightIdxIt); + } + if (!leftIdxIsMatched) + { + return FindSeteqCounterexampleResult{leftIdx, true}; + } + } + if (unmatchedRightIdxes.empty()) + return FindSeteqCounterexampleResult{std::nullopt, false}; + return FindSeteqCounterexampleResult{*unmatchedRightIdxes.begin(), false}; +} + +static DifferResult diffUnion(DifferEnvironment& env, TypeId left, TypeId right) +{ + const UnionType* leftUnion = get(left); + const UnionType* rightUnion = get(right); + LUAU_ASSERT(leftUnion); + LUAU_ASSERT(rightUnion); + + FindSeteqCounterexampleResult findSeteqCexResult = findSeteqCounterexample(env, leftUnion->options, rightUnion->options); + if (findSeteqCexResult.mismatchIdx.has_value()) + { + if (findSeteqCexResult.inLeft) + return DifferResult{DiffError{ + DiffError::Kind::MissingUnionMember, + DiffPathNodeLeaf::detailsUnionIndex(leftUnion->options[*findSeteqCexResult.mismatchIdx], *findSeteqCexResult.mismatchIdx), + DiffPathNodeLeaf::nullopts(), + getDevFixFriendlyName(env.rootLeft), + getDevFixFriendlyName(env.rootRight), + }}; + else + return DifferResult{DiffError{ + DiffError::Kind::MissingUnionMember, + DiffPathNodeLeaf::nullopts(), + DiffPathNodeLeaf::detailsUnionIndex(rightUnion->options[*findSeteqCexResult.mismatchIdx], *findSeteqCexResult.mismatchIdx), + getDevFixFriendlyName(env.rootLeft), + getDevFixFriendlyName(env.rootRight), + }}; + } + + // TODO: somehow detect mismatch index, likely using heuristics + + return DifferResult{}; +} + +static DifferResult diffIntersection(DifferEnvironment& env, TypeId left, TypeId right) +{ + const IntersectionType* leftIntersection = get(left); + const IntersectionType* rightIntersection = get(right); + LUAU_ASSERT(leftIntersection); + LUAU_ASSERT(rightIntersection); + + FindSeteqCounterexampleResult findSeteqCexResult = findSeteqCounterexample(env, leftIntersection->parts, rightIntersection->parts); + if (findSeteqCexResult.mismatchIdx.has_value()) + { + if (findSeteqCexResult.inLeft) + return DifferResult{DiffError{ + DiffError::Kind::MissingIntersectionMember, + DiffPathNodeLeaf::detailsUnionIndex(leftIntersection->parts[*findSeteqCexResult.mismatchIdx], *findSeteqCexResult.mismatchIdx), + DiffPathNodeLeaf::nullopts(), + getDevFixFriendlyName(env.rootLeft), + getDevFixFriendlyName(env.rootRight), + }}; + else + return DifferResult{DiffError{ + DiffError::Kind::MissingIntersectionMember, + DiffPathNodeLeaf::nullopts(), + DiffPathNodeLeaf::detailsUnionIndex(rightIntersection->parts[*findSeteqCexResult.mismatchIdx], *findSeteqCexResult.mismatchIdx), + getDevFixFriendlyName(env.rootLeft), + getDevFixFriendlyName(env.rootRight), + }}; + } + + // TODO: somehow detect mismatch index, likely using heuristics + + return DifferResult{}; +} + static DifferResult diffUsingEnv(DifferEnvironment& env, TypeId left, TypeId right) { left = follow(left); @@ -322,6 +550,10 @@ static DifferResult diffUsingEnv(DifferEnvironment& env, TypeId left, TypeId rig // Both left and right must be Any if either is Any for them to be equal! return DifferResult{}; } + else if (auto ln = get(left)) + { + return diffNegation(env, left, right); + } throw InternalCompilerError{"Unimplemented Simple TypeId variant for diffing"}; } @@ -336,6 +568,24 @@ static DifferResult diffUsingEnv(DifferEnvironment& env, TypeId left, TypeId rig { return diffFunction(env, left, right); } + if (auto lg = get(left)) + { + return diffGeneric(env, left, right); + } + if (auto lu = get(left)) + { + return diffUnion(env, left, right); + } + if (auto li = get(left)) + { + return diffIntersection(env, left, right); + } + if (auto le = get(left)) + { + // TODO: return debug-friendly result state + return DifferResult{}; + } + throw InternalCompilerError{"Unimplemented non-simple TypeId variant for diffing"}; } @@ -444,7 +694,7 @@ static DifferResult diffHandleFlattenedTail(DifferEnvironment& env, DiffError::K DifferResult diff(TypeId ty1, TypeId ty2) { - DifferEnvironment differEnv{ty1, ty2}; + DifferEnvironment differEnv{ty1, ty2, DenseHashMap{nullptr}}; return diffUsingEnv(differEnv, ty1, ty2); } @@ -452,7 +702,7 @@ bool isSimple(TypeId ty) { ty = follow(ty); // TODO: think about GenericType, etc. - return get(ty) || get(ty) || get(ty); + return get(ty) || get(ty) || get(ty) || get(ty); } } // namespace Luau diff --git a/Analysis/src/Frontend.cpp b/Analysis/src/Frontend.cpp index 2dea162b..362fcdcc 100644 --- a/Analysis/src/Frontend.cpp +++ b/Analysis/src/Frontend.cpp @@ -1141,22 +1141,26 @@ const SourceModule* Frontend::getSourceModule(const ModuleName& moduleName) cons ModulePtr check(const SourceModule& sourceModule, const std::vector& requireCycles, NotNull builtinTypes, NotNull iceHandler, NotNull moduleResolver, NotNull fileResolver, - const ScopePtr& parentScope, std::function prepareModuleScope, FrontendOptions options) + const ScopePtr& parentScope, std::function prepareModuleScope, FrontendOptions options, + TypeCheckLimits limits) { const bool recordJsonLog = FFlag::DebugLuauLogSolverToJson; return check(sourceModule, requireCycles, builtinTypes, iceHandler, moduleResolver, fileResolver, parentScope, std::move(prepareModuleScope), - options, recordJsonLog); + options, limits, recordJsonLog); } ModulePtr check(const SourceModule& sourceModule, const std::vector& requireCycles, NotNull builtinTypes, NotNull iceHandler, NotNull moduleResolver, NotNull fileResolver, const ScopePtr& parentScope, std::function prepareModuleScope, FrontendOptions options, - bool recordJsonLog) + TypeCheckLimits limits, bool recordJsonLog) { ModulePtr result = std::make_shared(); result->name = sourceModule.name; result->humanReadableName = sourceModule.humanReadableName; + result->internalTypes.owningModule = result.get(); + result->interfaceTypes.owningModule = result.get(); + iceHandler->moduleName = sourceModule.name; std::unique_ptr logger; @@ -1174,32 +1178,34 @@ ModulePtr check(const SourceModule& sourceModule, const std::vectorinternalTypes, builtinTypes, NotNull{&unifierState}}; - ConstraintGraphBuilder cgb{ - result, - &result->internalTypes, - moduleResolver, - builtinTypes, - iceHandler, - parentScope, - std::move(prepareModuleScope), - logger.get(), - NotNull{&dfg}, - }; + ConstraintGraphBuilder cgb{result, &result->internalTypes, moduleResolver, builtinTypes, iceHandler, parentScope, std::move(prepareModuleScope), + logger.get(), NotNull{&dfg}, requireCycles}; cgb.visit(sourceModule.root); result->errors = std::move(cgb.errors); - ConstraintSolver cs{ - NotNull{&normalizer}, NotNull(cgb.rootScope), borrowConstraints(cgb.constraints), result->name, moduleResolver, requireCycles, logger.get()}; + ConstraintSolver cs{NotNull{&normalizer}, NotNull(cgb.rootScope), borrowConstraints(cgb.constraints), result->humanReadableName, moduleResolver, + requireCycles, logger.get(), limits}; if (options.randomizeConstraintResolutionSeed) cs.randomize(*options.randomizeConstraintResolutionSeed); - cs.run(); + try + { + cs.run(); + } + catch (const TimeLimitError&) + { + result->timeout = true; + } + catch (const UserCancelError&) + { + result->cancelled = true; + } for (TypeError& e : cs.errors) result->errors.emplace_back(std::move(e)); @@ -1209,7 +1215,22 @@ ModulePtr check(const SourceModule& sourceModule, const std::vectorclonePublicInterface(builtinTypes, *iceHandler); - Luau::check(builtinTypes, NotNull{&unifierState}, logger.get(), sourceModule, result.get()); + if (result->timeout || result->cancelled) + { + // If solver was interrupted, skip typechecking and replace all module results with error-supressing types to avoid leaking blocked/pending types + ScopePtr moduleScope = result->getModuleScope(); + moduleScope->returnType = builtinTypes->errorRecoveryTypePack(); + + for (auto& [name, ty] : result->declaredGlobals) + ty = builtinTypes->errorRecoveryType(); + + for (auto& [name, tf] : result->exportedTypeBindings) + tf.type = builtinTypes->errorRecoveryType(); + } + else + { + Luau::check(builtinTypes, NotNull{&unifierState}, logger.get(), sourceModule, result.get()); + } // It would be nice if we could freeze the arenas before doing type // checking, but we'll have to do some work to get there. @@ -1248,7 +1269,7 @@ ModulePtr Frontend::check(const SourceModule& sourceModule, Mode mode, std::vect { return Luau::check(sourceModule, requireCycles, builtinTypes, NotNull{&iceHandler}, NotNull{forAutocomplete ? &moduleResolverForAutocomplete : &moduleResolver}, NotNull{fileResolver}, - environmentScope ? *environmentScope : globals.globalScope, prepareModuleScopeWrap, options, recordJsonLog); + environmentScope ? *environmentScope : globals.globalScope, prepareModuleScopeWrap, options, typeCheckLimits, recordJsonLog); } catch (const InternalCompilerError& err) { diff --git a/Analysis/src/Linter.cpp b/Analysis/src/Linter.cpp index d6aafda6..4abc1aa1 100644 --- a/Analysis/src/Linter.cpp +++ b/Analysis/src/Linter.cpp @@ -14,6 +14,8 @@ LUAU_FASTINTVARIABLE(LuauSuggestionDistance, 4) +LUAU_FASTFLAGVARIABLE(LuauLintNativeComment, false) + namespace Luau { @@ -2825,6 +2827,12 @@ static void lintComments(LintContext& context, const std::vector& ho "optimize directive uses unknown optimization level '%s', 0..2 expected", level); } } + else if (FFlag::LuauLintNativeComment && first == "native") + { + if (space != std::string::npos) + emitWarning(context, LintWarning::Code_CommentDirective, hc.location, + "native directive has extra symbols at the end of the line"); + } else { static const char* kHotComments[] = { @@ -2833,6 +2841,7 @@ static void lintComments(LintContext& context, const std::vector& ho "nonstrict", "strict", "optimize", + "native", }; if (const char* suggestion = fuzzyMatch(first, kHotComments, std::size(kHotComments))) diff --git a/Analysis/src/Normalize.cpp b/Analysis/src/Normalize.cpp index 33a8b6eb..bcad75b0 100644 --- a/Analysis/src/Normalize.cpp +++ b/Analysis/src/Normalize.cpp @@ -19,7 +19,6 @@ LUAU_FASTINTVARIABLE(LuauNormalizeIterationLimit, 1200); LUAU_FASTINTVARIABLE(LuauNormalizeCacheLimit, 100000); LUAU_FASTFLAGVARIABLE(LuauNormalizeBlockedTypes, false); LUAU_FASTFLAGVARIABLE(LuauNormalizeCyclicUnions, false); -LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution) LUAU_FASTFLAG(LuauTransitiveSubtyping) LUAU_FASTFLAG(DebugLuauReadWriteProperties) @@ -253,6 +252,14 @@ bool NormalizedType::shouldSuppressErrors() const return hasErrors() || get(tops); } +bool NormalizedType::hasTopTable() const +{ + return hasTables() && std::any_of(tables.begin(), tables.end(), [&](TypeId ty) { + auto primTy = get(ty); + return primTy && primTy->type == PrimitiveType::Type::Table; + }); +} + bool NormalizedType::hasTops() const { return !get(tops); diff --git a/Analysis/src/Quantify.cpp b/Analysis/src/Quantify.cpp index f7ed7619..0cc53d65 100644 --- a/Analysis/src/Quantify.cpp +++ b/Analysis/src/Quantify.cpp @@ -9,7 +9,6 @@ #include "Luau/VisitType.h" LUAU_FASTFLAG(DebugLuauSharedSelf) -LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution); namespace Luau { diff --git a/Analysis/src/Scope.cpp b/Analysis/src/Scope.cpp index 2de381be..bcd21d26 100644 --- a/Analysis/src/Scope.cpp +++ b/Analysis/src/Scope.cpp @@ -2,6 +2,8 @@ #include "Luau/Scope.h" +LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution); + namespace Luau { @@ -160,14 +162,12 @@ void Scope::inheritRefinements(const ScopePtr& childScope) dcrRefinements[k] = a; } } - else + + for (const auto& [k, a] : childScope->refinements) { - for (const auto& [k, a] : childScope->refinements) - { - Symbol symbol = getBaseSymbol(k); - if (lookup(symbol)) - refinements[k] = a; - } + Symbol symbol = getBaseSymbol(k); + if (lookup(symbol)) + refinements[k] = a; } } diff --git a/Analysis/src/Simplify.cpp b/Analysis/src/Simplify.cpp index e17df387..20a9fa57 100644 --- a/Analysis/src/Simplify.cpp +++ b/Analysis/src/Simplify.cpp @@ -364,7 +364,13 @@ Relation relate(TypeId left, TypeId right) if (auto ut = get(left)) return Relation::Intersects; else if (auto ut = get(right)) + { + std::vector opts; + for (TypeId part : ut) + if (relate(left, part) == Relation::Subset) + return Relation::Subset; return Relation::Intersects; + } if (auto rnt = get(right)) { diff --git a/Analysis/src/Symbol.cpp b/Analysis/src/Symbol.cpp index 5922bb50..4b808f19 100644 --- a/Analysis/src/Symbol.cpp +++ b/Analysis/src/Symbol.cpp @@ -3,9 +3,23 @@ #include "Luau/Common.h" +LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution) + namespace Luau { +bool Symbol::operator==(const Symbol& rhs) const +{ + if (local) + return local == rhs.local; + else if (global.value) + return rhs.global.value && global == rhs.global.value; // Subtlety: AstName::operator==(const char*) uses strcmp, not pointer identity. + else if (FFlag::DebugLuauDeferredConstraintResolution) + return !rhs.local && !rhs.global.value; // Reflexivity: we already know `this` Symbol is empty, so check that rhs is. + else + return false; +} + std::string toString(const Symbol& name) { if (name.local) diff --git a/Analysis/src/TxnLog.cpp b/Analysis/src/TxnLog.cpp index 8a9b3568..6446570c 100644 --- a/Analysis/src/TxnLog.cpp +++ b/Analysis/src/TxnLog.cpp @@ -9,8 +9,6 @@ #include #include -LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution) - namespace Luau { diff --git a/Analysis/src/TypeChecker2.cpp b/Analysis/src/TypeChecker2.cpp index b77f7f15..40a4bd0f 100644 --- a/Analysis/src/TypeChecker2.cpp +++ b/Analysis/src/TypeChecker2.cpp @@ -283,7 +283,8 @@ struct TypeChecker2 if (errors.empty()) noTypeFamilyErrors.insert(instance); - reportErrors(std::move(errors)); + if (!isErrorSuppressing(location, instance)) + reportErrors(std::move(errors)); return instance; } @@ -488,7 +489,7 @@ struct TypeChecker2 u.hideousFixMeGenericsAreActuallyFree = true; u.tryUnify(actualRetType, expectedRetType); - const bool ok = u.errors.empty() && u.log.empty(); + const bool ok = (u.errors.empty() && u.log.empty()) || isErrorSuppressing(ret->location, actualRetType, ret->location, expectedRetType); if (!ok) { @@ -526,9 +527,7 @@ struct TypeChecker2 TypeId valueType = value ? lookupType(value) : nullptr; if (valueType) { - ErrorVec errors = tryUnify(stack.back(), value->location, valueType, annotationType); - if (!errors.empty()) - reportErrors(std::move(errors)); + reportErrors(tryUnify(stack.back(), value->location, valueType, annotationType)); } visit(var->annotation); @@ -554,9 +553,7 @@ struct TypeChecker2 if (var->annotation) { TypeId varType = lookupAnnotation(var->annotation); - ErrorVec errors = tryUnify(stack.back(), value->location, valueTypes.head[j - i], varType); - if (!errors.empty()) - reportErrors(std::move(errors)); + reportErrors(tryUnify(stack.back(), value->location, valueTypes.head[j - i], varType)); visit(var->annotation); } @@ -764,6 +761,11 @@ struct TypeChecker2 } }; + const NormalizedType* iteratorNorm = normalizer.normalize(iteratorTy); + + if (!iteratorNorm) + reportError(NormalizationTooComplex{}, firstValue->location); + /* * If the first iterator argument is a function * * There must be 1 to 3 iterator arguments. Name them (nextTy, @@ -798,7 +800,7 @@ struct TypeChecker2 { // nothing } - else if (isOptional(iteratorTy)) + else if (isOptional(iteratorTy) && !(iteratorNorm && iteratorNorm->shouldSuppressErrors())) { reportError(OptionalValueAccess{iteratorTy}, forInStatement->values.data[0]->location); } @@ -833,7 +835,7 @@ struct TypeChecker2 { checkFunction(nextFtv, instantiatedIteratorTypes, true); } - else + else if (!isErrorSuppressing(forInStatement->values.data[0]->location, *instantiatedNextFn)) { reportError(CannotCallNonFunction{*instantiatedNextFn}, forInStatement->values.data[0]->location); } @@ -843,7 +845,7 @@ struct TypeChecker2 reportError(UnificationTooComplex{}, forInStatement->values.data[0]->location); } } - else + else if (!isErrorSuppressing(forInStatement->values.data[0]->location, *iterMmTy)) { // TODO: This will not tell the user that this is because the // metamethod isn't callable. This is not ideal, and we should @@ -859,7 +861,11 @@ struct TypeChecker2 reportError(UnificationTooComplex{}, forInStatement->values.data[0]->location); } } - else + else if (iteratorNorm && iteratorNorm->hasTopTable()) + { + // nothing + } + else if (!iteratorNorm || !iteratorNorm->shouldSuppressErrors()) { reportError(CannotCallNonFunction{iteratorTy}, forInStatement->values.data[0]->location); } @@ -882,7 +888,9 @@ struct TypeChecker2 if (get(lhsType)) continue; - if (!isSubtype(rhsType, lhsType, stack.back())) + + if (!isSubtype(rhsType, lhsType, stack.back()) && + !isErrorSuppressing(assign->vars.data[i]->location, lhsType, assign->values.data[i]->location, rhsType)) { reportError(TypeMismatch{lhsType, rhsType}, rhs->location); } @@ -1064,8 +1072,8 @@ struct TypeChecker2 void visitCall(AstExprCall* call) { TypePack args; - std::vector argLocs; - argLocs.reserve(call->args.size + 1); + std::vector argExprs; + argExprs.reserve(call->args.size + 1); TypeId* originalCallTy = module->astOriginalCallTypes.find(call); TypeId* selectedOverloadTy = module->astOverloadResolvedTypes.find(call); @@ -1088,18 +1096,18 @@ struct TypeChecker2 ice->ice("method call expression has no 'self'"); args.head.push_back(lookupType(indexExpr->expr)); - argLocs.push_back(indexExpr->expr->location); + argExprs.push_back(indexExpr->expr); } else if (findMetatableEntry(builtinTypes, module->errors, *originalCallTy, "__call", call->func->location)) { args.head.insert(args.head.begin(), lookupType(call->func)); - argLocs.push_back(call->func->location); + argExprs.push_back(call->func); } for (size_t i = 0; i < call->args.size; ++i) { AstExpr* arg = call->args.data[i]; - argLocs.push_back(arg->location); + argExprs.push_back(arg); TypeId* argTy = module->astTypes.find(arg); if (argTy) args.head.push_back(*argTy); @@ -1127,20 +1135,20 @@ struct TypeChecker2 call->location, }; - resolver.resolve(fnTy, &args, call->func->location, &argLocs); + resolver.resolve(fnTy, &args, call->func, &argExprs); + auto norm = normalizer.normalize(fnTy); + if (!norm) + reportError(NormalizationTooComplex{}, call->func->location); - if (!resolver.ok.empty()) + if (norm && norm->shouldSuppressErrors()) + return; // error suppressing function type! + else if (!resolver.ok.empty()) return; // We found a call that works, so this is ok. - else if (auto norm = normalizer.normalize(fnTy); !norm || !normalizer.isInhabited(norm)) - { - if (!norm) - reportError(NormalizationTooComplex{}, call->func->location); - else - return; // Ok. Calling an uninhabited type is no-op. - } + else if (!norm || !normalizer.isInhabited(norm)) + return; // Ok. Calling an uninhabited type is no-op. else if (!resolver.nonviableOverloads.empty()) { - if (resolver.nonviableOverloads.size() == 1) + if (resolver.nonviableOverloads.size() == 1 && !isErrorSuppressing(call->func->location, resolver.nonviableOverloads.front().first)) reportErrors(resolver.nonviableOverloads.front().second); else { @@ -1224,13 +1232,26 @@ struct TypeChecker2 InsertionOrderedMap> resolution; private: - template - std::optional tryUnify(const Location& location, Ty subTy, Ty superTy) + std::optional tryUnify(const Location& location, TypeId subTy, TypeId superTy, const LiteralProperties* literalProperties = nullptr) { Unifier u{normalizer, scope, location, Covariant}; u.ctx = CountMismatch::Arg; u.hideousFixMeGenericsAreActuallyFree = true; - u.enableScopeTests(); + u.enableNewSolver(); + u.tryUnify(subTy, superTy, /*isFunctionCall*/ false, /*isIntersection*/ false, literalProperties); + + if (u.errors.empty()) + return std::nullopt; + + return std::move(u.errors); + } + + std::optional tryUnify(const Location& location, TypePackId subTy, TypePackId superTy) + { + Unifier u{normalizer, scope, location, Covariant}; + u.ctx = CountMismatch::Arg; + u.hideousFixMeGenericsAreActuallyFree = true; + u.enableNewSolver(); u.tryUnify(subTy, superTy); if (u.errors.empty()) @@ -1240,7 +1261,7 @@ struct TypeChecker2 } std::pair checkOverload( - TypeId fnTy, const TypePack* args, Location fnLoc, const std::vector* argLocs, bool callMetamethodOk = true) + TypeId fnTy, const TypePack* args, AstExpr* fnLoc, const std::vector* argExprs, bool callMetamethodOk = true) { fnTy = follow(fnTy); @@ -1248,25 +1269,64 @@ struct TypeChecker2 if (get(fnTy) || get(fnTy) || get(fnTy)) return {Ok, {}}; else if (auto fn = get(fnTy)) - return checkOverload_(fnTy, fn, args, fnLoc, argLocs); // Intentionally split to reduce the stack pressure of this function. + return checkOverload_(fnTy, fn, args, fnLoc, argExprs); // Intentionally split to reduce the stack pressure of this function. else if (auto callMm = findMetatableEntry(builtinTypes, discard, fnTy, "__call", callLoc); callMm && callMetamethodOk) { // Calling a metamethod forwards the `fnTy` as self. TypePack withSelf = *args; withSelf.head.insert(withSelf.head.begin(), fnTy); - std::vector withSelfLocs = *argLocs; - withSelfLocs.insert(withSelfLocs.begin(), fnLoc); + std::vector withSelfExprs = *argExprs; + withSelfExprs.insert(withSelfExprs.begin(), fnLoc); - return checkOverload(*callMm, &withSelf, fnLoc, &withSelfLocs, /*callMetamethodOk=*/false); + return checkOverload(*callMm, &withSelf, fnLoc, &withSelfExprs, /*callMetamethodOk=*/false); } else return {TypeIsNotAFunction, {}}; // Intentionally empty. We can just fabricate the type error later on. } + static bool isLiteral(AstExpr* expr) + { + if (auto group = expr->as()) + return isLiteral(group->expr); + else if (auto assertion = expr->as()) + return isLiteral(assertion->expr); + + return + expr->is() || + expr->is() || + expr->is() || + expr->is() || + expr->is() || + expr->is(); + } + + static std::unique_ptr buildLiteralPropertiesSet(AstExpr* expr) + { + const AstExprTable* table = expr->as(); + if (!table) + return nullptr; + + std::unique_ptr result = std::make_unique(Name{}); + + for (const AstExprTable::Item& item : table->items) + { + if (item.kind != AstExprTable::Item::Record) + continue; + + AstExprConstantString* keyExpr = item.key->as(); + LUAU_ASSERT(keyExpr); + + if (isLiteral(item.value)) + result->insert(Name{keyExpr->value.begin(), keyExpr->value.end()}); + } + + return result; + } + LUAU_NOINLINE std::pair checkOverload_( - TypeId fnTy, const FunctionType* fn, const TypePack* args, Location fnLoc, const std::vector* argLocs) + TypeId fnTy, const FunctionType* fn, const TypePack* args, AstExpr* fnExpr, const std::vector* argExprs) { TxnLog fake; FamilyGraphReductionResult result = reduceFamilies(fnTy, callLoc, arena, builtinTypes, scope, normalizer, &fake, /*force=*/true); @@ -1286,9 +1346,11 @@ struct TypeChecker2 TypeId paramTy = *paramIter; TypeId argTy = args->head[argOffset]; - Location argLoc = argLocs->at(argOffset >= argLocs->size() ? argLocs->size() - 1 : argOffset); + AstExpr* argLoc = argExprs->at(argOffset >= argExprs->size() ? argExprs->size() - 1 : argOffset); - if (auto errors = tryUnify(argLoc, argTy, paramTy)) + std::unique_ptr literalProperties{buildLiteralPropertiesSet(argLoc)}; + + if (auto errors = tryUnify(argLoc->location, argTy, paramTy, literalProperties.get())) { // Since we're stopping right here, we need to decide if this is a nonviable overload or if there is an arity mismatch. // If it's a nonviable overload, then we need to keep going to get all type errors. @@ -1308,19 +1370,21 @@ struct TypeChecker2 // If we can iterate over the head of arguments, then we have exhausted the head of the parameters. LUAU_ASSERT(paramIter == end(fn->argTypes)); - Location argLoc = argLocs->at(argOffset >= argLocs->size() ? argLocs->size() - 1 : argOffset); + AstExpr* argExpr = argExprs->at(argOffset >= argExprs->size() ? argExprs->size() - 1 : argOffset); if (!paramIter.tail()) { auto [minParams, optMaxParams] = getParameterExtents(TxnLog::empty(), fn->argTypes); - TypeError error{argLoc, CountMismatch{minParams, optMaxParams, args->head.size(), CountMismatch::Arg, false}}; + TypeError error{argExpr->location, CountMismatch{minParams, optMaxParams, args->head.size(), CountMismatch::Arg, false}}; return {ArityMismatch, {error}}; } else if (auto vtp = get(follow(paramIter.tail()))) { - if (auto errors = tryUnify(argLoc, args->head[argOffset], vtp->ty)) + if (auto errors = tryUnify(argExpr->location, args->head[argOffset], vtp->ty)) argumentErrors.insert(argumentErrors.end(), errors->begin(), errors->end()); } + else if (get(follow(paramIter.tail()))) + argumentErrors.push_back(TypeError{argExpr->location, TypePackMismatch{fn->argTypes, arena->addTypePack(*args)}}); ++argOffset; } @@ -1333,18 +1397,18 @@ struct TypeChecker2 // It may have a tail, however, so check that. if (auto vtp = get(follow(args->tail))) { - Location argLoc = argLocs->at(argLocs->size() - 1); + AstExpr* argExpr = argExprs->at(argExprs->size() - 1); - if (auto errors = tryUnify(argLoc, vtp->ty, *paramIter)) + if (auto errors = tryUnify(argExpr->location, vtp->ty, *paramIter)) argumentErrors.insert(argumentErrors.end(), errors->begin(), errors->end()); } else if (!isOptional(*paramIter)) { - Location argLoc = argLocs->empty() ? fnLoc : argLocs->at(argLocs->size() - 1); + AstExpr* argExpr = argExprs->empty() ? fnExpr : argExprs->at(argExprs->size() - 1); // It is ok to have excess parameters as long as they are all optional. auto [minParams, optMaxParams] = getParameterExtents(TxnLog::empty(), fn->argTypes); - TypeError error{argLoc, CountMismatch{minParams, optMaxParams, args->head.size(), CountMismatch::Arg, false}}; + TypeError error{argExpr->location, CountMismatch{minParams, optMaxParams, args->head.size(), CountMismatch::Arg, false}}; return {ArityMismatch, {error}}; } @@ -1355,13 +1419,27 @@ struct TypeChecker2 LUAU_ASSERT(paramIter == end(fn->argTypes)); LUAU_ASSERT(argOffset == args->head.size()); + const Location argLoc = argExprs->empty() ? Location{} // TODO + : argExprs->at(argExprs->size() - 1)->location; + if (paramIter.tail() && args->tail) { - Location argLoc = argLocs->at(argLocs->size() - 1); - if (auto errors = tryUnify(argLoc, *args->tail, *paramIter.tail())) argumentErrors.insert(argumentErrors.end(), errors->begin(), errors->end()); } + else if (paramIter.tail()) + { + const TypePackId paramTail = follow(*paramIter.tail()); + + if (get(paramTail)) + { + argumentErrors.push_back(TypeError{argLoc, TypePackMismatch{fn->argTypes, arena->addTypePack(*args)}}); + } + else if (get(paramTail)) + { + // Nothing. This is ok. + } + } return {argumentErrors.empty() ? Ok : OverloadIsNonviable, argumentErrors}; } @@ -1409,14 +1487,14 @@ struct TypeChecker2 } public: - void resolve(TypeId fnTy, const TypePack* args, Location selfLoc, const std::vector* argLocs) + void resolve(TypeId fnTy, const TypePack* args, AstExpr* selfExpr, const std::vector* argExprs) { fnTy = follow(fnTy); auto it = get(fnTy); if (!it) { - auto [analysis, errors] = checkOverload(fnTy, args, selfLoc, argLocs); + auto [analysis, errors] = checkOverload(fnTy, args, selfExpr, argExprs); add(analysis, fnTy, std::move(errors)); return; } @@ -1426,7 +1504,7 @@ struct TypeChecker2 if (resolution.find(ty) != resolution.end()) continue; - auto [analysis, errors] = checkOverload(ty, args, selfLoc, argLocs); + auto [analysis, errors] = checkOverload(ty, args, selfExpr, argExprs); add(analysis, ty, std::move(errors)); } } @@ -1508,8 +1586,7 @@ struct TypeChecker2 return; } - // TODO! - visit(indexExpr->expr, ValueContext::LValue); + visit(indexExpr->expr, ValueContext::RValue); visit(indexExpr->index, ValueContext::RValue); NotNull scope = stack.back(); @@ -1576,7 +1653,8 @@ struct TypeChecker2 TypeId inferredArgTy = *argIt; TypeId annotatedArgTy = lookupAnnotation(arg->annotation); - if (!isSubtype(inferredArgTy, annotatedArgTy, stack.back())) + if (!isSubtype(inferredArgTy, annotatedArgTy, stack.back()) && + !isErrorSuppressing(arg->location, inferredArgTy, arg->annotation->location, annotatedArgTy)) { reportError(TypeMismatch{inferredArgTy, annotatedArgTy}, arg->location); } @@ -1610,7 +1688,7 @@ struct TypeChecker2 TypeId operandType = lookupType(expr->expr); TypeId resultType = lookupType(expr); - if (get(operandType) || get(operandType) || get(operandType)) + if (isErrorSuppressing(expr->expr->location, operandType)) return; if (auto it = kUnaryOpMetamethods.find(expr->op); it != kUnaryOpMetamethods.end()) @@ -1645,7 +1723,7 @@ struct TypeChecker2 TypeId expectedFunction = testArena.addType(FunctionType{expectedArgs, expectedRet}); ErrorVec errors = tryUnify(scope, expr->location, *mm, expectedFunction); - if (!errors.empty()) + if (!errors.empty() && !isErrorSuppressing(expr->expr->location, *firstArg, expr->expr->location, operandType)) { reportError(TypeMismatch{*firstArg, operandType}, expr->location); return; @@ -1660,7 +1738,10 @@ struct TypeChecker2 { DenseHashSet seen{nullptr}; int recursionCount = 0; + const NormalizedType* nty = normalizer.normalize(operandType); + if (nty && nty->shouldSuppressErrors()) + return; if (!hasLength(operandType, seen, &recursionCount)) { @@ -1719,6 +1800,8 @@ struct TypeChecker2 return leftType; else if (get(rightType) || get(rightType) || get(rightType)) return rightType; + else if ((normLeft && normLeft->shouldSuppressErrors()) || (normRight && normRight->shouldSuppressErrors())) + return builtinTypes->anyType; // we can't say anything better if it's error suppressing but not any or error alone. if ((get(leftType) || get(leftType) || get(leftType)) && !isEquality && !isLogical) { @@ -1933,6 +2016,9 @@ struct TypeChecker2 case AstExprBinary::Op::CompareLe: case AstExprBinary::Op::CompareLt: { + if (normLeft && normLeft->shouldSuppressErrors()) + return builtinTypes->numberType; + if (normLeft && normLeft->isExactlyNumber()) { reportErrors(tryUnify(scope, expr->right->location, rightType, builtinTypes->numberType)); @@ -2324,7 +2410,7 @@ struct TypeChecker2 TypeArena arena; Unifier u{NotNull{&normalizer}, scope, Location{}, Covariant}; u.hideousFixMeGenericsAreActuallyFree = genericsOkay; - u.enableScopeTests(); + u.enableNewSolver(); u.tryUnify(subTy, superTy); const bool ok = u.errors.empty() && u.log.empty(); @@ -2338,9 +2424,12 @@ struct TypeChecker2 Unifier u{NotNull{&normalizer}, scope, location, Covariant}; u.ctx = context; u.hideousFixMeGenericsAreActuallyFree = genericsOkay; - u.enableScopeTests(); + u.enableNewSolver(); u.tryUnify(subTy, superTy); + if (isErrorSuppressing(location, subTy, location, superTy)) + return {}; + return std::move(u.errors); } @@ -2376,6 +2465,7 @@ struct TypeChecker2 return; } + // if the type is error suppressing, we don't actually have any work left to do. if (norm->shouldSuppressErrors()) return; @@ -2542,6 +2632,50 @@ struct TypeChecker2 if (!candidates.empty()) data = TypeErrorData(UnknownPropButFoundLikeProp{utk->table, utk->key, candidates}); } + + bool isErrorSuppressing(Location loc, TypeId ty) + { + switch (shouldSuppressErrors(NotNull{&normalizer}, ty)) + { + case ErrorSuppression::DoNotSuppress: + return false; + case ErrorSuppression::Suppress: + return true; + case ErrorSuppression::NormalizationFailed: + reportError(NormalizationTooComplex{}, loc); + return false; + }; + + LUAU_ASSERT(false); + return false; // UNREACHABLE + } + + bool isErrorSuppressing(Location loc1, TypeId ty1, Location loc2, TypeId ty2) + { + return isErrorSuppressing(loc1, ty1) || isErrorSuppressing(loc2, ty2); + } + + bool isErrorSuppressing(Location loc, TypePackId tp) + { + switch (shouldSuppressErrors(NotNull{&normalizer}, tp)) + { + case ErrorSuppression::DoNotSuppress: + return false; + case ErrorSuppression::Suppress: + return true; + case ErrorSuppression::NormalizationFailed: + reportError(NormalizationTooComplex{}, loc); + return false; + }; + + LUAU_ASSERT(false); + return false; // UNREACHABLE + } + + bool isErrorSuppressing(Location loc1, TypePackId tp1, Location loc2, TypePackId tp2) + { + return isErrorSuppressing(loc1, tp1) || isErrorSuppressing(loc2, tp2); + } }; void check( diff --git a/Analysis/src/TypeInfer.cpp b/Analysis/src/TypeInfer.cpp index cfb0f21c..a8025096 100644 --- a/Analysis/src/TypeInfer.cpp +++ b/Analysis/src/TypeInfer.cpp @@ -36,6 +36,7 @@ LUAU_FASTFLAGVARIABLE(DebugLuauFreezeDuringUnification, false) LUAU_FASTFLAGVARIABLE(DebugLuauSharedSelf, false) LUAU_FASTFLAG(LuauInstantiateInSubtyping) LUAU_FASTFLAGVARIABLE(LuauAllowIndexClassParameters, false) +LUAU_FASTFLAGVARIABLE(LuauFixCyclicModuleExports, false) LUAU_FASTFLAG(LuauOccursIsntAlwaysFailure) LUAU_FASTFLAGVARIABLE(LuauTypecheckTypeguards, false) LUAU_FASTFLAGVARIABLE(LuauTinyControlFlowAnalysis, false) @@ -269,6 +270,8 @@ ModulePtr TypeChecker::checkWithoutRecursionCheck(const SourceModule& module, Mo currentModule.reset(new Module); currentModule->name = module.name; currentModule->humanReadableName = module.humanReadableName; + currentModule->internalTypes.owningModule = currentModule.get(); + currentModule->interfaceTypes.owningModule = currentModule.get(); currentModule->type = module.type; currentModule->allocator = module.allocator; currentModule->names = module.names; @@ -1193,6 +1196,19 @@ ControlFlow TypeChecker::check(const ScopePtr& scope, const AstStatLocal& local) { scope->importedTypeBindings[name] = module->exportedTypeBindings; scope->importedModules[name] = moduleInfo->name; + + if (FFlag::LuauFixCyclicModuleExports) + { + // Imported types of requires that transitively refer to current module have to be replaced with 'any' + for (const auto& [location, path] : requireCycles) + { + if (!path.empty() && path.front() == moduleInfo->name) + { + for (auto& [name, tf] : scope->importedTypeBindings[name]) + tf = TypeFun{{}, {}, anyType}; + } + } + } } // In non-strict mode we force the module type on the variable, in strict mode it is already unified diff --git a/Analysis/src/TypeUtils.cpp b/Analysis/src/TypeUtils.cpp index 9124e2fc..4f87de8f 100644 --- a/Analysis/src/TypeUtils.cpp +++ b/Analysis/src/TypeUtils.cpp @@ -295,4 +295,58 @@ TypeId stripNil(NotNull builtinTypes, TypeArena& arena, TypeId ty) return follow(ty); } +ErrorSuppression shouldSuppressErrors(NotNull normalizer, TypeId ty) +{ + const NormalizedType* normType = normalizer->normalize(ty); + + if (!normType) + return ErrorSuppression::NormalizationFailed; + + return (normType->shouldSuppressErrors()) ? ErrorSuppression::Suppress : ErrorSuppression::DoNotSuppress; +} + +ErrorSuppression shouldSuppressErrors(NotNull normalizer, TypePackId tp) +{ + auto [tys, tail] = flatten(tp); + + // check the head, one type at a time + for (TypeId ty : tys) + { + auto result = shouldSuppressErrors(normalizer, ty); + if (result != ErrorSuppression::DoNotSuppress) + return result; + } + + // check the tail if we have one and it's finite + if (tail && finite(*tail)) + return shouldSuppressErrors(normalizer, *tail); + + return ErrorSuppression::DoNotSuppress; +} + +// This is a useful helper because it is often the case that we are looking at specifically a pair of types that might suppress. +ErrorSuppression shouldSuppressErrors(NotNull normalizer, TypeId ty1, TypeId ty2) +{ + auto result = shouldSuppressErrors(normalizer, ty1); + + // if ty1 is do not suppress, ty2 determines our overall behavior + if (result == ErrorSuppression::DoNotSuppress) + return shouldSuppressErrors(normalizer, ty2); + + // otherwise, ty1 is either suppress or normalization failure which are both the appropriate overarching result + return result; +} + +ErrorSuppression shouldSuppressErrors(NotNull normalizer, TypePackId tp1, TypePackId tp2) +{ + auto result = shouldSuppressErrors(normalizer, tp1); + + // if tp1 is do not suppress, tp2 determines our overall behavior + if (result == ErrorSuppression::DoNotSuppress) + return shouldSuppressErrors(normalizer, tp2); + + // otherwise, tp1 is either suppress or normalization failure which are both the appropriate overarching result + return result; +} + } // namespace Luau diff --git a/Analysis/src/Unifier.cpp b/Analysis/src/Unifier.cpp index e54156fe..c1b5e45e 100644 --- a/Analysis/src/Unifier.cpp +++ b/Analysis/src/Unifier.cpp @@ -22,7 +22,6 @@ LUAU_FASTFLAGVARIABLE(LuauInstantiateInSubtyping, false) LUAU_FASTFLAGVARIABLE(LuauMaintainScopesInUnifier, false) LUAU_FASTFLAGVARIABLE(LuauTransitiveSubtyping, false) LUAU_FASTFLAGVARIABLE(LuauOccursIsntAlwaysFailure, false) -LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution) LUAU_FASTFLAG(LuauNormalizeBlockedTypes) LUAU_FASTFLAG(LuauAlwaysCommitInferencesOfFunctionCalls) @@ -50,7 +49,7 @@ struct PromoteTypeLevels final : TypeOnceVisitor template void promote(TID ty, T* t) { - if (FFlag::DebugLuauDeferredConstraintResolution && !t) + if (useScopes && !t) return; LUAU_ASSERT(t); @@ -369,7 +368,6 @@ static std::optional> getTableMatchT return std::nullopt; } -// TODO: Inline and clip with FFlag::DebugLuauDeferredConstraintResolution template static bool subsumes(bool useScopes, TY_A* left, TY_B* right) { @@ -406,11 +404,11 @@ Unifier::Unifier(NotNull normalizer, NotNull scope, const Loc LUAU_ASSERT(sharedState.iceHandler); } -void Unifier::tryUnify(TypeId subTy, TypeId superTy, bool isFunctionCall, bool isIntersection) +void Unifier::tryUnify(TypeId subTy, TypeId superTy, bool isFunctionCall, bool isIntersection, const LiteralProperties* literalProperties) { sharedState.counters.iterationCount = 0; - tryUnify_(subTy, superTy, isFunctionCall, isIntersection); + tryUnify_(subTy, superTy, isFunctionCall, isIntersection, literalProperties); } static bool isBlocked(const TxnLog& log, TypeId ty) @@ -425,7 +423,7 @@ static bool isBlocked(const TxnLog& log, TypePackId tp) return get(tp); } -void Unifier::tryUnify_(TypeId subTy, TypeId superTy, bool isFunctionCall, bool isIntersection) +void Unifier::tryUnify_(TypeId subTy, TypeId superTy, bool isFunctionCall, bool isIntersection, const LiteralProperties* literalProperties) { RecursionLimiter _ra(&sharedState.counters.recursionCount, sharedState.counters.recursionLimit); @@ -443,6 +441,16 @@ void Unifier::tryUnify_(TypeId subTy, TypeId superTy, bool isFunctionCall, bool if (superTy == subTy) return; + if (isBlocked(log, subTy) && isBlocked(log, superTy)) + { + blockedTypes.push_back(subTy); + blockedTypes.push_back(superTy); + } + else if (isBlocked(log, subTy)) + blockedTypes.push_back(subTy); + else if (isBlocked(log, superTy)) + blockedTypes.push_back(superTy); + if (log.get(superTy)) { // We do not report errors from reducing here. This is because we will @@ -470,7 +478,7 @@ void Unifier::tryUnify_(TypeId subTy, TypeId superTy, bool isFunctionCall, bool auto superFree = log.getMutable(superTy); auto subFree = log.getMutable(subTy); - if (superFree && subFree && subsumes(useScopes, superFree, subFree)) + if (superFree && subFree && subsumes(useNewSolver, superFree, subFree)) { if (!occursCheck(subTy, superTy, /* reversed = */ false)) log.replace(subTy, BoundType(superTy)); @@ -481,7 +489,7 @@ void Unifier::tryUnify_(TypeId subTy, TypeId superTy, bool isFunctionCall, bool { if (!occursCheck(superTy, subTy, /* reversed = */ true)) { - if (subsumes(useScopes, superFree, subFree)) + if (subsumes(useNewSolver, superFree, subFree)) { log.changeLevel(subTy, superFree->level); } @@ -495,7 +503,7 @@ void Unifier::tryUnify_(TypeId subTy, TypeId superTy, bool isFunctionCall, bool { // Unification can't change the level of a generic. auto subGeneric = log.getMutable(subTy); - if (subGeneric && !subsumes(useScopes, subGeneric, superFree)) + if (subGeneric && !subsumes(useNewSolver, subGeneric, superFree)) { // TODO: a more informative error message? CLI-39912 reportError(location, GenericError{"Generic subtype escaping scope"}); @@ -504,7 +512,7 @@ void Unifier::tryUnify_(TypeId subTy, TypeId superTy, bool isFunctionCall, bool if (!occursCheck(superTy, subTy, /* reversed = */ true)) { - promoteTypeLevels(log, types, superFree->level, superFree->scope, useScopes, subTy); + promoteTypeLevels(log, types, superFree->level, superFree->scope, useNewSolver, subTy); Widen widen{types, builtinTypes}; log.replace(superTy, BoundType(widen(subTy))); @@ -521,7 +529,7 @@ void Unifier::tryUnify_(TypeId subTy, TypeId superTy, bool isFunctionCall, bool // Unification can't change the level of a generic. auto superGeneric = log.getMutable(superTy); - if (superGeneric && !subsumes(useScopes, superGeneric, subFree)) + if (superGeneric && !subsumes(useNewSolver, superGeneric, subFree)) { // TODO: a more informative error message? CLI-39912 reportError(location, GenericError{"Generic supertype escaping scope"}); @@ -530,7 +538,7 @@ void Unifier::tryUnify_(TypeId subTy, TypeId superTy, bool isFunctionCall, bool if (!occursCheck(subTy, superTy, /* reversed = */ false)) { - promoteTypeLevels(log, types, subFree->level, subFree->scope, useScopes, superTy); + promoteTypeLevels(log, types, subFree->level, subFree->scope, useNewSolver, superTy); log.replace(subTy, BoundType(superTy)); } @@ -542,7 +550,7 @@ void Unifier::tryUnify_(TypeId subTy, TypeId superTy, bool isFunctionCall, bool auto superGeneric = log.getMutable(superTy); auto subGeneric = log.getMutable(subTy); - if (superGeneric && subGeneric && subsumes(useScopes, superGeneric, subGeneric)) + if (superGeneric && subGeneric && subsumes(useNewSolver, superGeneric, subGeneric)) { if (!occursCheck(subTy, superTy, /* reversed = */ false)) log.replace(subTy, BoundType(superTy)); @@ -637,16 +645,7 @@ void Unifier::tryUnify_(TypeId subTy, TypeId superTy, bool isFunctionCall, bool size_t errorCount = errors.size(); - if (isBlocked(log, subTy) && isBlocked(log, superTy)) - { - blockedTypes.push_back(subTy); - blockedTypes.push_back(superTy); - } - else if (isBlocked(log, subTy)) - blockedTypes.push_back(subTy); - else if (isBlocked(log, superTy)) - blockedTypes.push_back(superTy); - else if (const UnionType* subUnion = log.getMutable(subTy)) + if (const UnionType* subUnion = log.getMutable(subTy)) { tryUnifyUnionWithType(subTy, subUnion, superTy); } @@ -717,7 +716,7 @@ void Unifier::tryUnify_(TypeId subTy, TypeId superTy, bool isFunctionCall, bool else if (log.getMutable(superTy) && log.getMutable(subTy)) { - tryUnifyTables(subTy, superTy, isIntersection); + tryUnifyTables(subTy, superTy, isIntersection, literalProperties); } else if (log.get(superTy) && (log.get(subTy) || log.get(subTy))) { @@ -772,7 +771,7 @@ void Unifier::tryUnifyUnionWithType(TypeId subTy, const UnionType* subUnion, Typ Unifier innerState = makeChildUnifier(); innerState.tryUnify_(type, superTy); - if (FFlag::DebugLuauDeferredConstraintResolution) + if (useNewSolver) logs.push_back(std::move(innerState.log)); if (auto e = hasUnificationTooComplex(innerState.errors)) @@ -955,7 +954,7 @@ void Unifier::tryUnifyTypeWithUnion(TypeId subTy, TypeId superTy, const UnionTyp if (FFlag::LuauTransitiveSubtyping ? !innerState.failure : innerState.errors.empty()) { found = true; - if (FFlag::DebugLuauDeferredConstraintResolution) + if (useNewSolver) logs.push_back(std::move(innerState.log)); else { @@ -980,7 +979,7 @@ void Unifier::tryUnifyTypeWithUnion(TypeId subTy, TypeId superTy, const UnionTyp } } - if (FFlag::DebugLuauDeferredConstraintResolution) + if (useNewSolver) log.concatAsUnion(combineLogsIntoUnion(std::move(logs)), NotNull{types}); if (unificationTooComplex) @@ -1061,14 +1060,14 @@ void Unifier::tryUnifyTypeWithIntersection(TypeId subTy, TypeId superTy, const I firstFailedOption = {innerState.errors.front()}; } - if (FFlag::DebugLuauDeferredConstraintResolution) + if (useNewSolver) logs.push_back(std::move(innerState.log)); else log.concat(std::move(innerState.log)); failure |= innerState.failure; } - if (FFlag::DebugLuauDeferredConstraintResolution) + if (useNewSolver) log.concat(combineLogsIntoIntersection(std::move(logs))); if (unificationTooComplex) @@ -1118,7 +1117,7 @@ void Unifier::tryUnifyIntersectionWithType(TypeId subTy, const IntersectionType* } } - if (FFlag::DebugLuauDeferredConstraintResolution && normalize) + if (useNewSolver && normalize) { // We cannot normalize a type that contains blocked types. We have to // stop for now if we find any. @@ -1161,7 +1160,7 @@ void Unifier::tryUnifyIntersectionWithType(TypeId subTy, const IntersectionType* { found = true; errorsSuppressed = innerState.failure; - if (FFlag::DebugLuauDeferredConstraintResolution || (FFlag::LuauTransitiveSubtyping && innerState.failure)) + if (useNewSolver || (FFlag::LuauTransitiveSubtyping && innerState.failure)) logs.push_back(std::move(innerState.log)); else { @@ -1176,7 +1175,7 @@ void Unifier::tryUnifyIntersectionWithType(TypeId subTy, const IntersectionType* } } - if (FFlag::DebugLuauDeferredConstraintResolution) + if (useNewSolver) log.concat(combineLogsIntoIntersection(std::move(logs))); else if (FFlag::LuauTransitiveSubtyping && errorsSuppressed) log.concat(std::move(logs.front())); @@ -1296,7 +1295,7 @@ void Unifier::tryUnifyNormalizedTypes( } } - if (FFlag::DebugLuauDeferredConstraintResolution) + if (useNewSolver) { for (TypeId superTable : superNorm.tables) { @@ -1527,6 +1526,15 @@ struct WeirdIter return pack != nullptr && index < pack->head.size(); } + std::optional tail() const + { + if (!pack) + return packId; + + LUAU_ASSERT(index == pack->head.size()); + return pack->tail; + } + bool advance() { if (!pack) @@ -1588,9 +1596,9 @@ struct WeirdIter } }; -void Unifier::enableScopeTests() +void Unifier::enableNewSolver() { - useScopes = true; + useNewSolver = true; log.useScopes = true; } @@ -1664,25 +1672,26 @@ void Unifier::tryUnify_(TypePackId subTp, TypePackId superTp, bool isFunctionCal blockedTypePacks.push_back(superTp); } else if (isBlocked(log, subTp)) - { blockedTypePacks.push_back(subTp); - } else if (isBlocked(log, superTp)) - { blockedTypePacks.push_back(superTp); - } - if (log.getMutable(superTp)) + + if (auto superFree = log.getMutable(superTp)) { if (!occursCheck(superTp, subTp, /* reversed = */ true)) { Widen widen{types, builtinTypes}; + if (useNewSolver) + promoteTypeLevels(log, types, superFree->level, superFree->scope, /*useScopes*/ true, subTp); log.replace(superTp, Unifiable::Bound(widen(subTp))); } } - else if (log.getMutable(subTp)) + else if (auto subFree = log.getMutable(subTp)) { if (!occursCheck(subTp, superTp, /* reversed = */ false)) { + if (useNewSolver) + promoteTypeLevels(log, types, subFree->level, subFree->scope, /*useScopes*/ true, superTp); log.replace(subTp, Unifiable::Bound(superTp)); } } @@ -1771,28 +1780,74 @@ void Unifier::tryUnify_(TypePackId subTp, TypePackId superTp, bool isFunctionCal // If both are at the end, we're done if (!superIter.good() && !subIter.good()) { - const bool lFreeTail = superTpv->tail && log.getMutable(log.follow(*superTpv->tail)) != nullptr; - const bool rFreeTail = subTpv->tail && log.getMutable(log.follow(*subTpv->tail)) != nullptr; - if (lFreeTail && rFreeTail) + if (useNewSolver) { - tryUnify_(*subTpv->tail, *superTpv->tail); - } - else if (lFreeTail) - { - tryUnify_(emptyTp, *superTpv->tail); - } - else if (rFreeTail) - { - tryUnify_(emptyTp, *subTpv->tail); - } - else if (subTpv->tail && superTpv->tail) - { - if (log.getMutable(superIter.packId)) - tryUnifyVariadics(subIter.packId, superIter.packId, false, int(subIter.index)); - else if (log.getMutable(subIter.packId)) - tryUnifyVariadics(superIter.packId, subIter.packId, true, int(superIter.index)); + if (subIter.tail() && superIter.tail()) + tryUnify_(*subIter.tail(), *superIter.tail()); + else if (subIter.tail()) + { + const TypePackId subTail = log.follow(*subIter.tail()); + + if (log.get(subTail)) + tryUnify_(subTail, emptyTp); + else if (log.get(subTail)) + reportError(location, TypePackMismatch{subTail, emptyTp}); + else if (log.get(subTail) || log.get(subTail)) + { + // Nothing. This is ok. + } + else + { + ice("Unexpected subtype tail pack " + toString(subTail), location); + } + } + else if (superIter.tail()) + { + const TypePackId superTail = log.follow(*superIter.tail()); + + if (log.get(superTail)) + tryUnify_(emptyTp, superTail); + else if (log.get(superTail)) + reportError(location, TypePackMismatch{emptyTp, superTail}); + else if (log.get(superTail) || log.get(superTail)) + { + // Nothing. This is ok. + } + else + { + ice("Unexpected supertype tail pack " + toString(superTail), location); + } + } else + { + // Nothing. This is ok. + } + } + else + { + const bool lFreeTail = superTpv->tail && log.getMutable(log.follow(*superTpv->tail)) != nullptr; + const bool rFreeTail = subTpv->tail && log.getMutable(log.follow(*subTpv->tail)) != nullptr; + if (lFreeTail && rFreeTail) + { tryUnify_(*subTpv->tail, *superTpv->tail); + } + else if (lFreeTail) + { + tryUnify_(emptyTp, *superTpv->tail); + } + else if (rFreeTail) + { + tryUnify_(emptyTp, *subTpv->tail); + } + else if (subTpv->tail && superTpv->tail) + { + if (log.getMutable(superIter.packId)) + tryUnifyVariadics(subIter.packId, superIter.packId, false, int(subIter.index)); + else if (log.getMutable(subIter.packId)) + tryUnifyVariadics(superIter.packId, subIter.packId, true, int(superIter.index)); + else + tryUnify_(*subTpv->tail, *superTpv->tail); + } } break; @@ -2049,7 +2104,7 @@ struct Resetter } // namespace -void Unifier::tryUnifyTables(TypeId subTy, TypeId superTy, bool isIntersection) +void Unifier::tryUnifyTables(TypeId subTy, TypeId superTy, bool isIntersection, const LiteralProperties* literalProperties) { if (isPrim(log.follow(subTy), PrimitiveType::Table)) subTy = builtinTypes->emptyTableType; @@ -2134,7 +2189,8 @@ void Unifier::tryUnifyTables(TypeId subTy, TypeId superTy, bool isIntersection) { // TODO: read-only properties don't need invariance Resetter resetter{&variance}; - variance = Invariant; + if (!literalProperties || !literalProperties->contains(name)) + variance = Invariant; Unifier innerState = makeChildUnifier(); innerState.tryUnify_(r->second.type(), prop.type()); @@ -2150,7 +2206,8 @@ void Unifier::tryUnifyTables(TypeId subTy, TypeId superTy, bool isIntersection) // TODO: read-only indexers don't need invariance // TODO: really we should only allow this if prop.type is optional. Resetter resetter{&variance}; - variance = Invariant; + if (!literalProperties || !literalProperties->contains(name)) + variance = Invariant; Unifier innerState = makeChildUnifier(); innerState.tryUnify_(subTable->indexer->indexResultType, prop.type()); @@ -2213,10 +2270,17 @@ void Unifier::tryUnifyTables(TypeId subTy, TypeId superTy, bool isIntersection) // TODO: read-only indexers don't need invariance // TODO: really we should only allow this if prop.type is optional. Resetter resetter{&variance}; - variance = Invariant; + if (!literalProperties || !literalProperties->contains(name)) + variance = Invariant; Unifier innerState = makeChildUnifier(); - innerState.tryUnify_(superTable->indexer->indexResultType, prop.type()); + if (useNewSolver) + innerState.tryUnify_(prop.type(), superTable->indexer->indexResultType); + else + { + // Incredibly, the old solver depends on this bug somehow. + innerState.tryUnify_(superTable->indexer->indexResultType, prop.type()); + } checkChildUnifierTypeMismatch(innerState.errors, name, superTy, subTy); @@ -2478,7 +2542,7 @@ void Unifier::tryUnifyWithMetatable(TypeId subTy, TypeId superTy, bool reversed) { case TableState::Free: { - if (FFlag::DebugLuauDeferredConstraintResolution) + if (useNewSolver) { Unifier innerState = makeChildUnifier(); bool missingProperty = false; @@ -2843,8 +2907,8 @@ std::optional Unifier::findTablePropertyRespectingMeta(TypeId lhsType, N TxnLog Unifier::combineLogsIntoIntersection(std::vector logs) { - LUAU_ASSERT(FFlag::DebugLuauDeferredConstraintResolution); - TxnLog result(useScopes); + LUAU_ASSERT(useNewSolver); + TxnLog result(useNewSolver); for (TxnLog& log : logs) result.concatAsIntersections(std::move(log), NotNull{types}); return result; @@ -2852,7 +2916,7 @@ TxnLog Unifier::combineLogsIntoIntersection(std::vector logs) TxnLog Unifier::combineLogsIntoUnion(std::vector logs) { - TxnLog result(useScopes); + TxnLog result(useNewSolver); for (TxnLog& log : logs) result.concatAsUnion(std::move(log), NotNull{types}); return result; @@ -3012,8 +3076,8 @@ Unifier Unifier::makeChildUnifier() u.normalize = normalize; u.checkInhabited = checkInhabited; - if (useScopes) - u.enableScopeTests(); + if (useNewSolver) + u.enableNewSolver(); return u; } diff --git a/CLI/Repl.cpp b/CLI/Repl.cpp index df1b4eda..b2a523d9 100644 --- a/CLI/Repl.cpp +++ b/CLI/Repl.cpp @@ -274,9 +274,6 @@ std::string runCode(lua_State* L, const std::string& source) return error; } - if (codegen) - Luau::CodeGen::compile(L, -1); - lua_State* T = lua_newthread(L); lua_pushvalue(L, -2); diff --git a/CodeGen/include/Luau/CodeGen.h b/CodeGen/include/Luau/CodeGen.h index febd021c..002b4a99 100644 --- a/CodeGen/include/Luau/CodeGen.h +++ b/CodeGen/include/Luau/CodeGen.h @@ -12,12 +12,18 @@ namespace Luau namespace CodeGen { +enum CodeGenFlags +{ + // Only run native codegen for modules that have been marked with --!native + CodeGen_OnlyNativeModules = 1 << 0, +}; + bool isSupported(); void create(lua_State* L); // Builds target function and all inner functions -void compile(lua_State* L, int idx); +void compile(lua_State* L, int idx, unsigned int flags = 0); using AnnotatorFn = void (*)(void* context, std::string& result, int fid, int instpos); diff --git a/CodeGen/include/Luau/IrData.h b/CodeGen/include/Luau/IrData.h index 0b38743a..b950a8ec 100644 --- a/CodeGen/include/Luau/IrData.h +++ b/CodeGen/include/Luau/IrData.h @@ -77,6 +77,12 @@ enum class IrCmd : uint8_t // B: unsigned int (hash) GET_HASH_NODE_ADDR, + // Get pointer (TValue) to Closure upvalue. + // A: pointer or undef (Closure) + // B: UPn + // When undef is specified, uses current function Closure. + GET_CLOSURE_UPVAL_ADDR, + // Store a tag into TValue // A: Rn // B: tag @@ -542,10 +548,10 @@ enum class IrCmd : uint8_t FALLBACK_GETVARARGS, // Create closure from a child proto - // A: unsigned int (bytecode instruction index) - // B: Rn (dest) + // A: unsigned int (nups) + // B: pointer (table) // C: unsigned int (protoid) - FALLBACK_NEWCLOSURE, + NEWCLOSURE, // Create closure from a pre-created function object (reusing it unless environments diverge) // A: unsigned int (bytecode instruction index) @@ -600,6 +606,10 @@ enum class IrCmd : uint8_t // Returns the string name of a type either from a __type metatable field or just based on the tag, alternative for typeof(x) // A: Rn GET_TYPEOF, + + // Find or create an upval at the given level + // A: Rn (level) + FINDUPVAL, }; enum class IrConstKind : uint8_t diff --git a/CodeGen/include/Luau/IrUtils.h b/CodeGen/include/Luau/IrUtils.h index 6481342f..9a9b84b4 100644 --- a/CodeGen/include/Luau/IrUtils.h +++ b/CodeGen/include/Luau/IrUtils.h @@ -150,6 +150,7 @@ inline bool hasResult(IrCmd cmd) case IrCmd::GET_ARR_ADDR: case IrCmd::GET_SLOT_NODE_ADDR: case IrCmd::GET_HASH_NODE_ADDR: + case IrCmd::GET_CLOSURE_UPVAL_ADDR: case IrCmd::ADD_INT: case IrCmd::SUB_INT: case IrCmd::ADD_NUM: @@ -192,6 +193,8 @@ inline bool hasResult(IrCmd cmd) case IrCmd::INVOKE_LIBM: case IrCmd::GET_TYPE: case IrCmd::GET_TYPEOF: + case IrCmd::NEWCLOSURE: + case IrCmd::FINDUPVAL: return true; default: break; diff --git a/CodeGen/include/luacodegen.h b/CodeGen/include/luacodegen.h index 654fc2c9..9de15e63 100644 --- a/CodeGen/include/luacodegen.h +++ b/CodeGen/include/luacodegen.h @@ -6,7 +6,7 @@ #define LUACODEGEN_API extern #endif -struct lua_State; +typedef struct lua_State lua_State; // returns 1 if Luau code generator is supported, 0 otherwise LUACODEGEN_API int luau_codegen_supported(void); diff --git a/CodeGen/src/CodeGen.cpp b/CodeGen/src/CodeGen.cpp index cdb761c6..602130f1 100644 --- a/CodeGen/src/CodeGen.cpp +++ b/CodeGen/src/CodeGen.cpp @@ -239,7 +239,7 @@ void create(lua_State* L) ecb->enter = onEnter; } -void compile(lua_State* L, int idx) +void compile(lua_State* L, int idx, unsigned int flags) { LUAU_ASSERT(lua_isLfunction(L, idx)); const TValue* func = luaA_toobject(L, idx); @@ -249,6 +249,13 @@ void compile(lua_State* L, int idx) if (!data) return; + Proto* root = clvalue(func)->l.p; + if ((flags & CodeGen_OnlyNativeModules) != 0 && (root->flags & LPF_NATIVE_MODULE) == 0) + return; + + std::vector protos; + gatherFunctions(protos, root); + #if defined(__aarch64__) static unsigned int cpuFeatures = getCpuFeaturesA64(); A64::AssemblyBuilderA64 build(/* logText= */ false, cpuFeatures); @@ -256,9 +263,6 @@ void compile(lua_State* L, int idx) X64::AssemblyBuilderX64 build(/* logText= */ false); #endif - std::vector protos; - gatherFunctions(protos, clvalue(func)->l.p); - ModuleHelpers helpers; #if defined(__aarch64__) A64::assembleHelpers(build, helpers); diff --git a/CodeGen/src/IrAnalysis.cpp b/CodeGen/src/IrAnalysis.cpp index cf7161ef..62c0b8ab 100644 --- a/CodeGen/src/IrAnalysis.cpp +++ b/CodeGen/src/IrAnalysis.cpp @@ -427,9 +427,6 @@ static RegisterSet computeBlockLiveInRegSet(IrFunction& function, const IrBlock& case IrCmd::FALLBACK_GETVARARGS: defRange(vmRegOp(inst.b), function.intOp(inst.c)); break; - case IrCmd::FALLBACK_NEWCLOSURE: - def(inst.b); - break; case IrCmd::FALLBACK_DUPCLOSURE: def(inst.b); break; @@ -448,6 +445,10 @@ static RegisterSet computeBlockLiveInRegSet(IrFunction& function, const IrBlock& use(inst.a); break; + case IrCmd::FINDUPVAL: + use(inst.a); + break; + default: // All instructions which reference registers have to be handled explicitly LUAU_ASSERT(inst.a.kind != IrOpKind::VmReg); diff --git a/CodeGen/src/IrBuilder.cpp b/CodeGen/src/IrBuilder.cpp index 04318eff..52d0a0b5 100644 --- a/CodeGen/src/IrBuilder.cpp +++ b/CodeGen/src/IrBuilder.cpp @@ -452,7 +452,7 @@ void IrBuilder::translateInst(LuauOpcode op, const Instruction* pc, int i) inst(IrCmd::FALLBACK_GETVARARGS, constUint(i), vmReg(LUAU_INSN_A(*pc)), constInt(LUAU_INSN_B(*pc) - 1)); break; case LOP_NEWCLOSURE: - inst(IrCmd::FALLBACK_NEWCLOSURE, constUint(i), vmReg(LUAU_INSN_A(*pc)), constUint(LUAU_INSN_D(*pc))); + translateInstNewClosure(*this, pc, i); break; case LOP_DUPCLOSURE: inst(IrCmd::FALLBACK_DUPCLOSURE, constUint(i), vmReg(LUAU_INSN_A(*pc)), vmConst(LUAU_INSN_D(*pc))); diff --git a/CodeGen/src/IrDump.cpp b/CodeGen/src/IrDump.cpp index c44cd8eb..ce0cbfb3 100644 --- a/CodeGen/src/IrDump.cpp +++ b/CodeGen/src/IrDump.cpp @@ -61,6 +61,12 @@ static const char* getTagName(uint8_t tag) return "tuserdata"; case LUA_TTHREAD: return "tthread"; + case LUA_TPROTO: + return "tproto"; + case LUA_TUPVAL: + return "tupval"; + case LUA_TDEADKEY: + return "tdeadkey"; default: LUAU_ASSERT(!"Unknown type tag"); LUAU_UNREACHABLE(); @@ -93,6 +99,8 @@ const char* getCmdName(IrCmd cmd) return "GET_SLOT_NODE_ADDR"; case IrCmd::GET_HASH_NODE_ADDR: return "GET_HASH_NODE_ADDR"; + case IrCmd::GET_CLOSURE_UPVAL_ADDR: + return "GET_CLOSURE_UPVAL_ADDR"; case IrCmd::STORE_TAG: return "STORE_TAG"; case IrCmd::STORE_POINTER: @@ -267,8 +275,8 @@ const char* getCmdName(IrCmd cmd) return "FALLBACK_PREPVARARGS"; case IrCmd::FALLBACK_GETVARARGS: return "FALLBACK_GETVARARGS"; - case IrCmd::FALLBACK_NEWCLOSURE: - return "FALLBACK_NEWCLOSURE"; + case IrCmd::NEWCLOSURE: + return "NEWCLOSURE"; case IrCmd::FALLBACK_DUPCLOSURE: return "FALLBACK_DUPCLOSURE"; case IrCmd::FALLBACK_FORGPREP: @@ -303,6 +311,8 @@ const char* getCmdName(IrCmd cmd) return "GET_TYPE"; case IrCmd::GET_TYPEOF: return "GET_TYPEOF"; + case IrCmd::FINDUPVAL: + return "FINDUPVAL"; } LUAU_UNREACHABLE(); diff --git a/CodeGen/src/IrLoweringA64.cpp b/CodeGen/src/IrLoweringA64.cpp index 92cb49ad..3c247abd 100644 --- a/CodeGen/src/IrLoweringA64.cpp +++ b/CodeGen/src/IrLoweringA64.cpp @@ -314,6 +314,14 @@ void IrLoweringA64::lowerInst(IrInst& inst, uint32_t index, IrBlock& next) build.add(inst.regA64, inst.regA64, zextReg(temp2), kLuaNodeSizeLog2); break; } + case IrCmd::GET_CLOSURE_UPVAL_ADDR: + { + inst.regA64 = regs.allocReuse(KindA64::x, index, {inst.a}); + RegisterA64 cl = inst.a.kind == IrOpKind::Undef ? rClosure : regOp(inst.a); + + build.add(inst.regA64, cl, uint16_t(offsetof(Closure, l.uprefs) + sizeof(TValue) * vmUpvalueOp(inst.b))); + break; + } case IrCmd::STORE_TAG: { RegisterA64 temp = regs.allocTemp(KindA64::w); @@ -1044,13 +1052,20 @@ void IrLoweringA64::lowerInst(IrInst& inst, uint32_t index, IrBlock& next) bool continueInVm = (inst.d.kind == IrOpKind::Constant && intOp(inst.d)); Label fresh; // used when guard aborts execution or jumps to a VM exit Label& fail = continueInVm ? helpers.exitContinueVmClearNativeFlag : getTargetLabel(inst.c, fresh); + + // To support DebugLuauAbortingChecks, CHECK_TAG with VmReg has to be handled + RegisterA64 tag = inst.a.kind == IrOpKind::VmReg ? regs.allocTemp(KindA64::w) : regOp(inst.a); + + if (inst.a.kind == IrOpKind::VmReg) + build.ldr(tag, mem(rBase, vmRegOp(inst.a) * sizeof(TValue) + offsetof(TValue, tt))); + if (tagOp(inst.b) == 0) { - build.cbnz(regOp(inst.a), fail); + build.cbnz(tag, fail); } else { - build.cmp(regOp(inst.a), tagOp(inst.b)); + build.cmp(tag, tagOp(inst.b)); build.b(ConditionA64::NotEqual, fail); } if (!continueInVm) @@ -1517,13 +1532,26 @@ void IrLoweringA64::lowerInst(IrInst& inst, uint32_t index, IrBlock& next) regs.spill(build, index); emitFallback(build, offsetof(NativeContext, executeGETVARARGS), uintOp(inst.a)); break; - case IrCmd::FALLBACK_NEWCLOSURE: - LUAU_ASSERT(inst.b.kind == IrOpKind::VmReg); - LUAU_ASSERT(inst.c.kind == IrOpKind::Constant); + case IrCmd::NEWCLOSURE: + { + RegisterA64 reg = regOp(inst.b); // note: we need to call regOp before spill so that we don't do redundant reloads - regs.spill(build, index); - emitFallback(build, offsetof(NativeContext, executeNEWCLOSURE), uintOp(inst.a)); + regs.spill(build, index, {reg}); + build.mov(x2, reg); + + build.mov(x0, rState); + build.mov(w1, uintOp(inst.a)); + + build.ldr(x3, mem(rClosure, offsetof(Closure, l.p))); + build.ldr(x3, mem(x3, offsetof(Proto, p))); + build.ldr(x3, mem(x3, sizeof(Proto*) * uintOp(inst.c))); + + build.ldr(x4, mem(rNativeContext, offsetof(NativeContext, luaF_newLclosure))); + build.blr(x4); + + inst.regA64 = regs.takeReg(x0, index); break; + } case IrCmd::FALLBACK_DUPCLOSURE: LUAU_ASSERT(inst.b.kind == IrOpKind::VmReg); LUAU_ASSERT(inst.c.kind == IrOpKind::VmConst); @@ -1743,6 +1771,18 @@ void IrLoweringA64::lowerInst(IrInst& inst, uint32_t index, IrBlock& next) break; } + case IrCmd::FINDUPVAL: + { + regs.spill(build, index); + build.mov(x0, rState); + build.add(x1, rBase, uint16_t(vmRegOp(inst.a) * sizeof(TValue))); + build.ldr(x2, mem(rNativeContext, offsetof(NativeContext, luaF_findupval))); + build.blr(x2); + + inst.regA64 = regs.takeReg(x0, index); + break; + } + // To handle unsupported instructions, add "case IrCmd::OP" and make sure to set error = true! } diff --git a/CodeGen/src/IrLoweringX64.cpp b/CodeGen/src/IrLoweringX64.cpp index 670d6066..e791e55d 100644 --- a/CodeGen/src/IrLoweringX64.cpp +++ b/CodeGen/src/IrLoweringX64.cpp @@ -186,14 +186,40 @@ void IrLoweringX64::lowerInst(IrInst& inst, uint32_t index, IrBlock& next) build.add(inst.regX64, tmp.reg); break; }; + case IrCmd::GET_CLOSURE_UPVAL_ADDR: + { + inst.regX64 = regs.allocRegOrReuse(SizeX64::qword, index, {inst.a}); + + if (inst.a.kind == IrOpKind::Undef) + { + build.mov(inst.regX64, sClosure); + } + else + { + RegisterX64 cl = regOp(inst.a); + if (inst.regX64 != cl) + build.mov(inst.regX64, cl); + } + + build.add(inst.regX64, offsetof(Closure, l.uprefs) + sizeof(TValue) * vmUpvalueOp(inst.b)); + break; + } case IrCmd::STORE_TAG: if (inst.b.kind == IrOpKind::Constant) - build.mov(luauRegTag(vmRegOp(inst.a)), tagOp(inst.b)); + { + if (inst.a.kind == IrOpKind::Inst) + build.mov(dword[regOp(inst.a) + offsetof(TValue, tt)], tagOp(inst.b)); + else + build.mov(luauRegTag(vmRegOp(inst.a)), tagOp(inst.b)); + } else LUAU_ASSERT(!"Unsupported instruction form"); break; case IrCmd::STORE_POINTER: - build.mov(luauRegValue(vmRegOp(inst.a)), regOp(inst.b)); + if (inst.a.kind == IrOpKind::Inst) + build.mov(qword[regOp(inst.a) + offsetof(TValue, value)], regOp(inst.b)); + else + build.mov(luauRegValue(vmRegOp(inst.a)), regOp(inst.b)); break; case IrCmd::STORE_DOUBLE: if (inst.b.kind == IrOpKind::Constant) @@ -1207,12 +1233,25 @@ void IrLoweringX64::lowerInst(IrInst& inst, uint32_t index, IrBlock& next) emitFallback(regs, build, offsetof(NativeContext, executeGETVARARGS), uintOp(inst.a)); break; - case IrCmd::FALLBACK_NEWCLOSURE: - LUAU_ASSERT(inst.b.kind == IrOpKind::VmReg); - LUAU_ASSERT(inst.c.kind == IrOpKind::Constant); + case IrCmd::NEWCLOSURE: + { + ScopedRegX64 tmp2{regs, SizeX64::qword}; + build.mov(tmp2.reg, sClosure); + build.mov(tmp2.reg, qword[tmp2.reg + offsetof(Closure, l.p)]); + build.mov(tmp2.reg, qword[tmp2.reg + offsetof(Proto, p)]); + build.mov(tmp2.reg, qword[tmp2.reg + sizeof(Proto*) * uintOp(inst.c)]); - emitFallback(regs, build, offsetof(NativeContext, executeNEWCLOSURE), uintOp(inst.a)); + IrCallWrapperX64 callWrap(regs, build, index); + callWrap.addArgument(SizeX64::qword, rState); + callWrap.addArgument(SizeX64::dword, uintOp(inst.a), inst.a); + callWrap.addArgument(SizeX64::qword, regOp(inst.b), inst.b); + callWrap.addArgument(SizeX64::qword, tmp2); + + callWrap.call(qword[rNativeContext + offsetof(NativeContext, luaF_newLclosure)]); + + inst.regX64 = regs.takeReg(rax, index); break; + } case IrCmd::FALLBACK_DUPCLOSURE: LUAU_ASSERT(inst.b.kind == IrOpKind::VmReg); LUAU_ASSERT(inst.c.kind == IrOpKind::VmConst); @@ -1412,6 +1451,17 @@ void IrLoweringX64::lowerInst(IrInst& inst, uint32_t index, IrBlock& next) break; } + case IrCmd::FINDUPVAL: + { + IrCallWrapperX64 callWrap(regs, build); + callWrap.addArgument(SizeX64::qword, rState); + callWrap.addArgument(SizeX64::qword, luauRegAddress(vmRegOp(inst.a))); + callWrap.call(qword[rNativeContext + offsetof(NativeContext, luaF_findupval)]); + + inst.regX64 = regs.takeReg(rax, index); + break; + } + // Pseudo instructions case IrCmd::NOP: case IrCmd::SUBSTITUTE: diff --git a/CodeGen/src/IrTranslation.cpp b/CodeGen/src/IrTranslation.cpp index 5cde510f..63e756e1 100644 --- a/CodeGen/src/IrTranslation.cpp +++ b/CodeGen/src/IrTranslation.cpp @@ -1213,5 +1213,61 @@ void translateInstOrX(IrBuilder& build, const Instruction* pc, int pcpos, IrOp c } } +void translateInstNewClosure(IrBuilder& build, const Instruction* pc, int pcpos) +{ + LUAU_ASSERT(unsigned(LUAU_INSN_D(*pc)) < unsigned(build.function.proto->sizep)); + + int ra = LUAU_INSN_A(*pc); + Proto* pv = build.function.proto->p[LUAU_INSN_D(*pc)]; + + build.inst(IrCmd::SET_SAVEDPC, build.constUint(pcpos + 1)); + + IrOp env = build.inst(IrCmd::LOAD_ENV); + IrOp ncl = build.inst(IrCmd::NEWCLOSURE, build.constUint(pv->nups), env, build.constUint(LUAU_INSN_D(*pc))); + + build.inst(IrCmd::STORE_POINTER, build.vmReg(ra), ncl); + build.inst(IrCmd::STORE_TAG, build.vmReg(ra), build.constTag(LUA_TFUNCTION)); + + for (int ui = 0; ui < pv->nups; ++ui) + { + Instruction uinsn = pc[ui + 1]; + LUAU_ASSERT(LUAU_INSN_OP(uinsn) == LOP_CAPTURE); + + IrOp dst = build.inst(IrCmd::GET_CLOSURE_UPVAL_ADDR, ncl, build.vmUpvalue(ui)); + + switch (LUAU_INSN_A(uinsn)) + { + case LCT_VAL: + { + IrOp src = build.inst(IrCmd::LOAD_TVALUE, build.vmReg(LUAU_INSN_B(uinsn))); + build.inst(IrCmd::STORE_TVALUE, dst, src); + break; + } + + case LCT_REF: + { + IrOp src = build.inst(IrCmd::FINDUPVAL, build.vmReg(LUAU_INSN_B(uinsn))); + build.inst(IrCmd::STORE_POINTER, dst, src); + build.inst(IrCmd::STORE_TAG, dst, build.constTag(LUA_TUPVAL)); + break; + } + + case LCT_UPVAL: + { + IrOp src = build.inst(IrCmd::GET_CLOSURE_UPVAL_ADDR, build.undef(), build.vmUpvalue(LUAU_INSN_B(uinsn))); + IrOp load = build.inst(IrCmd::LOAD_TVALUE, src); + build.inst(IrCmd::STORE_TVALUE, dst, load); + break; + } + + default: + LUAU_ASSERT(!"Unknown upvalue capture type"); + LUAU_UNREACHABLE(); // improves switch() codegen by eliding opcode bounds checks + } + } + + build.inst(IrCmd::CHECK_GC); +} + } // namespace CodeGen } // namespace Luau diff --git a/CodeGen/src/IrTranslation.h b/CodeGen/src/IrTranslation.h index 0c24b27d..aff18b30 100644 --- a/CodeGen/src/IrTranslation.h +++ b/CodeGen/src/IrTranslation.h @@ -65,6 +65,7 @@ void translateInstCapture(IrBuilder& build, const Instruction* pc, int pcpos); void translateInstNamecall(IrBuilder& build, const Instruction* pc, int pcpos); void translateInstAndX(IrBuilder& build, const Instruction* pc, int pcpos, IrOp c); void translateInstOrX(IrBuilder& build, const Instruction* pc, int pcpos, IrOp c); +void translateInstNewClosure(IrBuilder& build, const Instruction* pc, int pcpos); inline int getOpLength(LuauOpcode op) { diff --git a/CodeGen/src/IrUtils.cpp b/CodeGen/src/IrUtils.cpp index 2395fb1e..310c15b8 100644 --- a/CodeGen/src/IrUtils.cpp +++ b/CodeGen/src/IrUtils.cpp @@ -38,6 +38,7 @@ IrValueKind getCmdValueKind(IrCmd cmd) case IrCmd::GET_ARR_ADDR: case IrCmd::GET_SLOT_NODE_ADDR: case IrCmd::GET_HASH_NODE_ADDR: + case IrCmd::GET_CLOSURE_UPVAL_ADDR: return IrValueKind::Pointer; case IrCmd::STORE_TAG: case IrCmd::STORE_POINTER: @@ -141,7 +142,9 @@ IrValueKind getCmdValueKind(IrCmd cmd) case IrCmd::FALLBACK_NAMECALL: case IrCmd::FALLBACK_PREPVARARGS: case IrCmd::FALLBACK_GETVARARGS: - case IrCmd::FALLBACK_NEWCLOSURE: + return IrValueKind::None; + case IrCmd::NEWCLOSURE: + return IrValueKind::Pointer; case IrCmd::FALLBACK_DUPCLOSURE: case IrCmd::FALLBACK_FORGPREP: return IrValueKind::None; @@ -164,6 +167,8 @@ IrValueKind getCmdValueKind(IrCmd cmd) case IrCmd::GET_TYPE: case IrCmd::GET_TYPEOF: return IrValueKind::Pointer; + case IrCmd::FINDUPVAL: + return IrValueKind::Pointer; } LUAU_UNREACHABLE(); diff --git a/CodeGen/src/IrValueLocationTracking.cpp b/CodeGen/src/IrValueLocationTracking.cpp index e94a4347..0ed7c388 100644 --- a/CodeGen/src/IrValueLocationTracking.cpp +++ b/CodeGen/src/IrValueLocationTracking.cpp @@ -77,7 +77,6 @@ void IrValueLocationTracking::beforeInstLowering(IrInst& inst) case IrCmd::FALLBACK_GETVARARGS: invalidateRestoreVmRegs(vmRegOp(inst.b), function.intOp(inst.c)); break; - case IrCmd::FALLBACK_NEWCLOSURE: case IrCmd::FALLBACK_DUPCLOSURE: invalidateRestoreOp(inst.b); break; @@ -109,6 +108,8 @@ void IrValueLocationTracking::beforeInstLowering(IrInst& inst) case IrCmd::FALLBACK_PREPVARARGS: case IrCmd::ADJUST_STACK_TO_TOP: case IrCmd::GET_TYPEOF: + case IrCmd::NEWCLOSURE: + case IrCmd::FINDUPVAL: break; // These instrucitons read VmReg only after optimizeMemoryOperandsX64 diff --git a/CodeGen/src/NativeState.cpp b/CodeGen/src/NativeState.cpp index 14c1acd9..65984562 100644 --- a/CodeGen/src/NativeState.cpp +++ b/CodeGen/src/NativeState.cpp @@ -56,6 +56,8 @@ void initFunctions(NativeState& data) data.context.luaC_step = luaC_step; data.context.luaF_close = luaF_close; + data.context.luaF_findupval = luaF_findupval; + data.context.luaF_newLclosure = luaF_newLclosure; data.context.luaT_gettm = luaT_gettm; data.context.luaT_objtypenamestr = luaT_objtypenamestr; diff --git a/CodeGen/src/NativeState.h b/CodeGen/src/NativeState.h index a2393bbf..1a039812 100644 --- a/CodeGen/src/NativeState.h +++ b/CodeGen/src/NativeState.h @@ -52,6 +52,8 @@ struct NativeContext size_t (*luaC_step)(lua_State* L, bool assist) = nullptr; void (*luaF_close)(lua_State* L, StkId level) = nullptr; + UpVal* (*luaF_findupval)(lua_State* L, StkId level) = nullptr; + Closure* (*luaF_newLclosure)(lua_State* L, int nelems, Table* e, Proto* p) = nullptr; const TValue* (*luaT_gettm)(Table* events, TMS event, TString* ename) = nullptr; const TString* (*luaT_objtypenamestr)(lua_State* L, const TValue* o) = nullptr; diff --git a/CodeGen/src/OptimizeConstProp.cpp b/CodeGen/src/OptimizeConstProp.cpp index 72869ad1..758518a2 100644 --- a/CodeGen/src/OptimizeConstProp.cpp +++ b/CodeGen/src/OptimizeConstProp.cpp @@ -415,6 +415,8 @@ static void handleBuiltinEffects(ConstPropState& state, LuauBuiltinFunction bfid case LBF_RAWLEN: case LBF_BIT32_EXTRACTK: case LBF_GETMETATABLE: + case LBF_TONUMBER: + case LBF_TOSTRING: break; case LBF_SETMETATABLE: state.invalidateHeap(); // TODO: only knownNoMetatable is affected and we might know which one @@ -760,6 +762,7 @@ static void constPropInInst(ConstPropState& state, IrBuilder& build, IrFunction& case IrCmd::GET_ARR_ADDR: case IrCmd::GET_SLOT_NODE_ADDR: case IrCmd::GET_HASH_NODE_ADDR: + case IrCmd::GET_CLOSURE_UPVAL_ADDR: break; case IrCmd::ADD_INT: case IrCmd::SUB_INT: @@ -823,6 +826,7 @@ static void constPropInInst(ConstPropState& state, IrBuilder& build, IrFunction& case IrCmd::BITXOR_UINT: case IrCmd::BITOR_UINT: case IrCmd::BITNOT_UINT: + break; case IrCmd::BITLSHIFT_UINT: case IrCmd::BITRSHIFT_UINT: case IrCmd::BITARSHIFT_UINT: @@ -833,6 +837,7 @@ static void constPropInInst(ConstPropState& state, IrBuilder& build, IrFunction& case IrCmd::INVOKE_LIBM: case IrCmd::GET_TYPE: case IrCmd::GET_TYPEOF: + case IrCmd::FINDUPVAL: break; case IrCmd::JUMP_CMP_ANY: @@ -923,8 +928,7 @@ static void constPropInInst(ConstPropState& state, IrBuilder& build, IrFunction& case IrCmd::FALLBACK_GETVARARGS: state.invalidateRegisterRange(vmRegOp(inst.b), function.intOp(inst.c)); break; - case IrCmd::FALLBACK_NEWCLOSURE: - state.invalidate(inst.b); + case IrCmd::NEWCLOSURE: break; case IrCmd::FALLBACK_DUPCLOSURE: state.invalidate(inst.b); diff --git a/Common/include/Luau/Bytecode.h b/Common/include/Luau/Bytecode.h index 7b3a057b..976dd04f 100644 --- a/Common/include/Luau/Bytecode.h +++ b/Common/include/Luau/Bytecode.h @@ -44,6 +44,7 @@ // Version 1: Baseline version for the open-source release. Supported until 0.521. // Version 2: Adds Proto::linedefined. Supported until 0.544. // Version 3: Adds FORGPREP/JUMPXEQK* and enhances AUX encoding for FORGLOOP. Removes FORGLOOP_NEXT/INEXT and JUMPIFEQK/JUMPIFNOTEQK. Currently supported. +// Version 4: Adds Proto::flags and typeinfo. Currently supported. // Bytecode opcode, part of the instruction header enum LuauOpcode @@ -543,6 +544,10 @@ enum LuauBuiltinFunction // get/setmetatable LBF_GETMETATABLE, LBF_SETMETATABLE, + + // tonumber/tostring + LBF_TONUMBER, + LBF_TOSTRING, }; // Capture type, used in LOP_CAPTURE @@ -552,3 +557,10 @@ enum LuauCaptureType LCT_REF, LCT_UPVAL, }; + +// Proto flag bitmask, stored in Proto::flags +enum LuauProtoFlag +{ + // used to tag main proto for modules with --!native + LPF_NATIVE_MODULE = 1 << 0, +}; diff --git a/Compiler/src/Builtins.cpp b/Compiler/src/Builtins.cpp index 80fe0b6d..4ec083bb 100644 --- a/Compiler/src/Builtins.cpp +++ b/Compiler/src/Builtins.cpp @@ -4,6 +4,9 @@ #include "Luau/Bytecode.h" #include "Luau/Compiler.h" +LUAU_FASTFLAGVARIABLE(LuauCompileBuiltinTonumber, false) +LUAU_FASTFLAGVARIABLE(LuauCompileBuiltinTostring, false) + namespace Luau { namespace Compile @@ -69,6 +72,11 @@ static int getBuiltinFunctionId(const Builtin& builtin, const CompileOptions& op if (builtin.isGlobal("setmetatable")) return LBF_SETMETATABLE; + if (FFlag::LuauCompileBuiltinTonumber && builtin.isGlobal("tonumber")) + return LBF_TONUMBER; + if (FFlag::LuauCompileBuiltinTostring && builtin.isGlobal("tostring")) + return LBF_TOSTRING; + if (builtin.object == "math") { if (builtin.method == "abs") @@ -257,10 +265,10 @@ BuiltinInfo getBuiltinInfo(int bfid) case LBF_MATH_ABS: case LBF_MATH_ACOS: case LBF_MATH_ASIN: - return {1, 1}; + return {1, 1, BuiltinInfo::Flag_NoneSafe}; case LBF_MATH_ATAN2: - return {2, 1}; + return {2, 1, BuiltinInfo::Flag_NoneSafe}; case LBF_MATH_ATAN: case LBF_MATH_CEIL: @@ -269,19 +277,19 @@ BuiltinInfo getBuiltinInfo(int bfid) case LBF_MATH_DEG: case LBF_MATH_EXP: case LBF_MATH_FLOOR: - return {1, 1}; + return {1, 1, BuiltinInfo::Flag_NoneSafe}; case LBF_MATH_FMOD: - return {2, 1}; + return {2, 1, BuiltinInfo::Flag_NoneSafe}; case LBF_MATH_FREXP: - return {1, 2}; + return {1, 2, BuiltinInfo::Flag_NoneSafe}; case LBF_MATH_LDEXP: - return {2, 1}; + return {2, 1, BuiltinInfo::Flag_NoneSafe}; case LBF_MATH_LOG10: - return {1, 1}; + return {1, 1, BuiltinInfo::Flag_NoneSafe}; case LBF_MATH_LOG: return {-1, 1}; // 1 or 2 parameters @@ -291,10 +299,10 @@ BuiltinInfo getBuiltinInfo(int bfid) return {-1, 1}; // variadic case LBF_MATH_MODF: - return {1, 2}; + return {1, 2, BuiltinInfo::Flag_NoneSafe}; case LBF_MATH_POW: - return {2, 1}; + return {2, 1, BuiltinInfo::Flag_NoneSafe}; case LBF_MATH_RAD: case LBF_MATH_SINH: @@ -302,16 +310,16 @@ BuiltinInfo getBuiltinInfo(int bfid) case LBF_MATH_SQRT: case LBF_MATH_TANH: case LBF_MATH_TAN: - return {1, 1}; + return {1, 1, BuiltinInfo::Flag_NoneSafe}; case LBF_BIT32_ARSHIFT: - return {2, 1}; + return {2, 1, BuiltinInfo::Flag_NoneSafe}; case LBF_BIT32_BAND: return {-1, 1}; // variadic case LBF_BIT32_BNOT: - return {1, 1}; + return {1, 1, BuiltinInfo::Flag_NoneSafe}; case LBF_BIT32_BOR: case LBF_BIT32_BXOR: @@ -323,14 +331,14 @@ BuiltinInfo getBuiltinInfo(int bfid) case LBF_BIT32_LROTATE: case LBF_BIT32_LSHIFT: - return {2, 1}; + return {2, 1, BuiltinInfo::Flag_NoneSafe}; case LBF_BIT32_REPLACE: return {-1, 1}; // 3 or 4 parameters case LBF_BIT32_RROTATE: case LBF_BIT32_RSHIFT: - return {2, 1}; + return {2, 1, BuiltinInfo::Flag_NoneSafe}; case LBF_TYPE: return {1, 1}; @@ -342,7 +350,7 @@ BuiltinInfo getBuiltinInfo(int bfid) return {-1, 1}; // variadic case LBF_STRING_LEN: - return {1, 1}; + return {1, 1, BuiltinInfo::Flag_NoneSafe}; case LBF_TYPEOF: return {1, 1}; @@ -351,11 +359,11 @@ BuiltinInfo getBuiltinInfo(int bfid) return {-1, 1}; // 2 or 3 parameters case LBF_MATH_CLAMP: - return {3, 1}; + return {3, 1, BuiltinInfo::Flag_NoneSafe}; case LBF_MATH_SIGN: case LBF_MATH_ROUND: - return {1, 1}; + return {1, 1, BuiltinInfo::Flag_NoneSafe}; case LBF_RAWSET: return {3, 1}; @@ -375,22 +383,28 @@ BuiltinInfo getBuiltinInfo(int bfid) case LBF_BIT32_COUNTLZ: case LBF_BIT32_COUNTRZ: - return {1, 1}; + return {1, 1, BuiltinInfo::Flag_NoneSafe}; case LBF_SELECT_VARARG: return {-1, -1}; // variadic case LBF_RAWLEN: - return {1, 1}; + return {1, 1, BuiltinInfo::Flag_NoneSafe}; case LBF_BIT32_EXTRACTK: - return {3, 1}; + return {3, 1, BuiltinInfo::Flag_NoneSafe}; case LBF_GETMETATABLE: return {1, 1}; case LBF_SETMETATABLE: return {2, 1}; + + case LBF_TONUMBER: + return {-1, 1}; // 1 or 2 parameters + + case LBF_TOSTRING: + return {1, 1}; }; LUAU_UNREACHABLE(); diff --git a/Compiler/src/Builtins.h b/Compiler/src/Builtins.h index e179218a..2d783248 100644 --- a/Compiler/src/Builtins.h +++ b/Compiler/src/Builtins.h @@ -41,8 +41,17 @@ void analyzeBuiltins(DenseHashMap& result, const DenseHashMap struct BuiltinInfo { + enum Flags + { + // none-safe builtins are builtins that have the same behavior for arguments that are nil or none + // this allows the compiler to compile calls to builtins more efficiently in certain cases + // for example, math.abs(x()) may compile x() as if it returns one value; if it returns no values, abs() will get nil instead of none + Flag_NoneSafe = 1 << 0, + }; + int params; int results; + unsigned int flags; }; BuiltinInfo getBuiltinInfo(int bfid); diff --git a/Compiler/src/Compiler.cpp b/Compiler/src/Compiler.cpp index fe65f67a..23deec9b 100644 --- a/Compiler/src/Compiler.cpp +++ b/Compiler/src/Compiler.cpp @@ -27,6 +27,9 @@ LUAU_FASTINTVARIABLE(LuauCompileInlineThresholdMaxBoost, 300) LUAU_FASTINTVARIABLE(LuauCompileInlineDepth, 5) LUAU_FASTFLAGVARIABLE(LuauCompileFunctionType, false) +LUAU_FASTFLAGVARIABLE(LuauCompileNativeComment, false) + +LUAU_FASTFLAGVARIABLE(LuauCompileFixBuiltinArity, false) namespace Luau { @@ -187,7 +190,7 @@ struct Compiler return node->as(); } - uint32_t compileFunction(AstExprFunction* func) + uint32_t compileFunction(AstExprFunction* func, uint8_t protoflags) { LUAU_TIMETRACE_SCOPE("Compiler::compileFunction", "Compiler"); @@ -262,7 +265,7 @@ struct Compiler if (bytecode.getInstructionCount() > kMaxInstructionCount) CompileError::raise(func->location, "Exceeded function instruction limit; split the function into parts to compile"); - bytecode.endFunction(uint8_t(stackSize), uint8_t(upvals.size())); + bytecode.endFunction(uint8_t(stackSize), uint8_t(upvals.size()), protoflags); Function& f = functions[func]; f.id = fid; @@ -792,8 +795,19 @@ struct Compiler { if (!isExprMultRet(expr->args.data[expr->args.size - 1])) return compileExprFastcallN(expr, target, targetCount, targetTop, multRet, regs, bfid); - else if (options.optimizationLevel >= 2 && int(expr->args.size) == getBuiltinInfo(bfid).params) - return compileExprFastcallN(expr, target, targetCount, targetTop, multRet, regs, bfid); + else if (options.optimizationLevel >= 2) + { + if (FFlag::LuauCompileFixBuiltinArity) + { + // when a builtin is none-safe with matching arity, even if the last expression returns 0 or >1 arguments, + // we can rely on the behavior of the function being the same (none-safe means nil and none are interchangeable) + BuiltinInfo info = getBuiltinInfo(bfid); + if (int(expr->args.size) == info.params && (info.flags & BuiltinInfo::Flag_NoneSafe) != 0) + return compileExprFastcallN(expr, target, targetCount, targetTop, multRet, regs, bfid); + } + else if (int(expr->args.size) == getBuiltinInfo(bfid).params) + return compileExprFastcallN(expr, target, targetCount, targetTop, multRet, regs, bfid); + } } if (expr->self) @@ -3147,7 +3161,7 @@ struct Compiler } } - // compute expressions with side effects for lulz + // compute expressions with side effects for (size_t i = stat->vars.size; i < stat->values.size; ++i) { RegScope rsi(this); @@ -3834,11 +3848,20 @@ void compileOrThrow(BytecodeBuilder& bytecode, const ParseResult& parseResult, c LUAU_ASSERT(parseResult.errors.empty()); CompileOptions options = inputOptions; + uint8_t mainFlags = 0; for (const HotComment& hc : parseResult.hotcomments) + { if (hc.header && hc.content.compare(0, 9, "optimize ") == 0) options.optimizationLevel = std::max(0, std::min(2, atoi(hc.content.c_str() + 9))); + if (FFlag::LuauCompileNativeComment && hc.header && hc.content == "native") + { + mainFlags |= LPF_NATIVE_MODULE; + options.optimizationLevel = 2; // note: this might be removed in the future in favor of --!optimize + } + } + AstStatBlock* root = parseResult.root; Compiler compiler(bytecode, options); @@ -3884,12 +3907,12 @@ void compileOrThrow(BytecodeBuilder& bytecode, const ParseResult& parseResult, c root->visit(&functionVisitor); for (AstExprFunction* expr : functions) - compiler.compileFunction(expr); + compiler.compileFunction(expr, 0); AstExprFunction main(root->location, /*generics= */ AstArray(), /*genericPacks= */ AstArray(), /* self= */ nullptr, AstArray(), /* vararg= */ true, /* varargLocation= */ Luau::Location(), root, /* functionDepth= */ 0, /* debugname= */ AstName()); - uint32_t mainid = compiler.compileFunction(&main); + uint32_t mainid = compiler.compileFunction(&main, mainFlags); const Compiler::Function* mainf = compiler.functions.find(&main); LUAU_ASSERT(mainf && mainf->upvals.empty()); diff --git a/Compiler/src/CostModel.cpp b/Compiler/src/CostModel.cpp index ffc1cb1f..2f7af6ea 100644 --- a/Compiler/src/CostModel.cpp +++ b/Compiler/src/CostModel.cpp @@ -6,6 +6,8 @@ #include +LUAU_FASTFLAGVARIABLE(LuauAssignmentHasCost, false) + namespace Luau { namespace Compile @@ -302,12 +304,14 @@ struct CostVisitor : AstVisitor return false; } - bool visit(AstStat* node) override + bool visit(AstStatIf* node) override { - if (node->is()) + // unconditional 'else' may require a jump after the 'if' body + // note: this ignores cases when 'then' always terminates and also assumes comparison requires an extra instruction which may be false + if (!FFlag::LuauAssignmentHasCost) result += 2; - else if (node->is() || node->is()) - result += 1; + else + result += 1 + (node->elsebody && !node->elsebody->is()); return true; } @@ -333,7 +337,21 @@ struct CostVisitor : AstVisitor for (size_t i = 0; i < node->vars.size; ++i) assign(node->vars.data[i]); - return true; + if (!FFlag::LuauAssignmentHasCost) + return true; + + for (size_t i = 0; i < node->vars.size || i < node->values.size; ++i) + { + Cost ac; + if (i < node->vars.size) + ac += model(node->vars.data[i]); + if (i < node->values.size) + ac += model(node->values.data[i]); + // local->local or constant->local assignment is not free + result += ac.model == 0 ? Cost(1) : ac; + } + + return false; } bool visit(AstStatCompoundAssign* node) override @@ -345,6 +363,20 @@ struct CostVisitor : AstVisitor return true; } + + bool visit(AstStatBreak* node) override + { + result += 1; + + return false; + } + + bool visit(AstStatContinue* node) override + { + result += 1; + + return false; + } }; uint64_t modelCost(AstNode* root, AstLocal* const* vars, size_t varCount, const DenseHashMap& builtins) diff --git a/Sources.cmake b/Sources.cmake index 2a58f061..c1230f30 100644 --- a/Sources.cmake +++ b/Sources.cmake @@ -186,6 +186,7 @@ target_sources(Luau.Analysis PRIVATE Analysis/include/Luau/TypeArena.h Analysis/include/Luau/TypeAttach.h Analysis/include/Luau/TypeChecker2.h + Analysis/include/Luau/TypeCheckLimits.h Analysis/include/Luau/TypedAllocator.h Analysis/include/Luau/TypeFamily.h Analysis/include/Luau/TypeInfer.h diff --git a/VM/include/luaconf.h b/VM/include/luaconf.h index 2045768a..dcb785b6 100644 --- a/VM/include/luaconf.h +++ b/VM/include/luaconf.h @@ -123,7 +123,7 @@ // enables callbacks to redirect code execution from Luau VM to a custom implementation #ifndef LUA_CUSTOM_EXECUTION -#define LUA_CUSTOM_EXECUTION 0 +#define LUA_CUSTOM_EXECUTION 1 #endif // }================================================================== diff --git a/VM/src/laux.cpp b/VM/src/laux.cpp index b4490fff..0b9787a0 100644 --- a/VM/src/laux.cpp +++ b/VM/src/laux.cpp @@ -336,7 +336,7 @@ const char* luaL_findtable(lua_State* L, int idx, const char* fname, int szhint) const char* luaL_typename(lua_State* L, int idx) { const TValue* obj = luaA_toobject(L, idx); - return luaT_objtypename(L, obj); + return obj ? luaT_objtypename(L, obj) : "no value"; } /* diff --git a/VM/src/lbuiltins.cpp b/VM/src/lbuiltins.cpp index e0dc8a38..c893d603 100644 --- a/VM/src/lbuiltins.cpp +++ b/VM/src/lbuiltins.cpp @@ -23,6 +23,8 @@ #endif #endif +LUAU_DYNAMIC_FASTFLAGVARIABLE(LuauFastcallGC, false) + // luauF functions implement FASTCALL instruction that performs a direct execution of some builtin functions from the VM // The rule of thumb is that FASTCALL functions can not call user code, yield, fail, or reallocate stack. // If types of the arguments mismatch, luauF_* needs to return -1 and the execution will fall back to the usual call path @@ -830,6 +832,8 @@ static int luauF_char(lua_State* L, StkId res, TValue* arg0, int nresults, StkId if (nparams < int(sizeof(buffer)) && nresults <= 1) { + if (DFFlag::LuauFastcallGC && luaC_needsGC(L)) + return -1; // we can't call luaC_checkGC so fall back to C implementation if (nparams >= 1) { @@ -900,6 +904,9 @@ static int luauF_sub(lua_State* L, StkId res, TValue* arg0, int nresults, StkId int i = int(nvalue(args)); int j = int(nvalue(args + 1)); + if (DFFlag::LuauFastcallGC && luaC_needsGC(L)) + return -1; // we can't call luaC_checkGC so fall back to C implementation + if (i >= 1 && j >= i && unsigned(j - 1) < unsigned(ts->len)) { setsvalue(L, res, luaS_newlstr(L, getstr(ts) + (i - 1), j - i + 1)); @@ -1247,6 +1254,73 @@ static int luauF_setmetatable(lua_State* L, StkId res, TValue* arg0, int nresult return -1; } +static int luauF_tonumber(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams == 1 && nresults <= 1) + { + double num; + + if (ttisnumber(arg0)) + { + setnvalue(res, nvalue(arg0)); + return 1; + } + else if (ttisstring(arg0) && luaO_str2d(svalue(arg0), &num)) + { + setnvalue(res, num); + return 1; + } + else + { + setnilvalue(res); + return 1; + } + } + + return -1; +} + +static int luauF_tostring(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) +{ + if (nparams >= 1 && nresults <= 1) + { + switch (ttype(arg0)) + { + case LUA_TNIL: + { + TString* s = L->global->ttname[LUA_TNIL]; + setsvalue(L, res, s); + return 1; + } + case LUA_TBOOLEAN: + { + TString* s = bvalue(arg0) ? luaS_newliteral(L, "true") : luaS_newliteral(L, "false"); + setsvalue(L, res, s); + return 1; + } + case LUA_TNUMBER: + { + if (DFFlag::LuauFastcallGC && luaC_needsGC(L)) + return -1; // we can't call luaC_checkGC so fall back to C implementation + + char s[LUAI_MAXNUM2STR]; + char* e = luai_num2str(s, nvalue(arg0)); + setsvalue(L, res, luaS_newlstr(L, s, e - s)); + return 1; + } + case LUA_TSTRING: + { + setsvalue(L, res, tsvalue(arg0)); + return 1; + } + } + + // fall back to generic C implementation + } + + return -1; +} + static int luauF_missing(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) { return -1; @@ -1411,6 +1485,9 @@ const luau_FastFunction luauF_table[256] = { luauF_getmetatable, luauF_setmetatable, + luauF_tonumber, + luauF_tostring, + // When adding builtins, add them above this line; what follows is 64 "dummy" entries with luauF_missing fallback. // This is important so that older versions of the runtime that don't support newer builtins automatically fall back via luauF_missing. // Given the builtin addition velocity this should always provide a larger compatibility window than bytecode versions suggest. diff --git a/VM/src/lfunc.cpp b/VM/src/lfunc.cpp index 88a3e40a..276598fe 100644 --- a/VM/src/lfunc.cpp +++ b/VM/src/lfunc.cpp @@ -36,6 +36,7 @@ Proto* luaF_newproto(lua_State* L) f->execdata = NULL; f->exectarget = 0; f->typeinfo = NULL; + f->userdata = NULL; return f; } diff --git a/VM/src/lgc.h b/VM/src/lgc.h index ec7a6828..1ebb01d2 100644 --- a/VM/src/lgc.h +++ b/VM/src/lgc.h @@ -73,10 +73,12 @@ #define luaC_white(g) cast_to(uint8_t, ((g)->currentwhite) & WHITEBITS) +#define luaC_needsGC(L) (L->global->totalbytes >= L->global->GCthreshold) + #define luaC_checkGC(L) \ { \ condhardstacktests(luaD_reallocstack(L, L->stacksize - EXTRA_STACK)); \ - if (L->global->totalbytes >= L->global->GCthreshold) \ + if (luaC_needsGC(L)) \ { \ condhardmemtests(luaC_validate(L), 1); \ luaC_step(L, true); \ diff --git a/VM/src/lmathlib.cpp b/VM/src/lmathlib.cpp index 2d4e3277..fe7b1a12 100644 --- a/VM/src/lmathlib.cpp +++ b/VM/src/lmathlib.cpp @@ -7,6 +7,8 @@ #include #include +LUAU_FASTFLAGVARIABLE(LuauFasterNoise, false) + #undef PI #define PI (3.14159265358979323846) #define RADIANS_PER_DEGREE (PI / 180.0) @@ -275,6 +277,7 @@ static int math_randomseed(lua_State* L) return 0; } +// TODO: Delete with LuauFasterNoise static const unsigned char kPerlin[512] = {151, 160, 137, 91, 90, 15, 131, 13, 201, 95, 96, 53, 194, 233, 7, 225, 140, 36, 103, 30, 69, 142, 8, 99, 37, 240, 21, 10, 23, 190, 6, 148, 247, 120, 234, 75, 0, 26, 197, 62, 94, 252, 219, 203, 117, 35, 11, 32, 57, 177, 33, 88, 237, 149, 56, 87, 174, 20, 125, 136, 171, 168, 68, 175, 74, 165, 71, 134, 139, 48, 27, 166, 77, 146, 158, 231, 83, 111, 229, 122, 60, 211, 133, 230, 220, 105, 92, 41, @@ -295,18 +298,32 @@ static const unsigned char kPerlin[512] = {151, 160, 137, 91, 90, 15, 131, 13, 2 106, 157, 184, 84, 204, 176, 115, 121, 50, 45, 127, 4, 150, 254, 138, 236, 205, 93, 222, 114, 67, 29, 24, 72, 243, 141, 128, 195, 78, 66, 215, 61, 156, 180}; -static float fade(float t) +static const unsigned char kPerlinHash[257] = {151, 160, 137, 91, 90, 15, 131, 13, 201, 95, 96, 53, 194, 233, 7, 225, 140, 36, 103, 30, 69, 142, 8, 99, + 37, 240, 21, 10, 23, 190, 6, 148, 247, 120, 234, 75, 0, 26, 197, 62, 94, 252, 219, 203, 117, 35, 11, 32, 57, 177, 33, 88, 237, 149, 56, 87, 174, + 20, 125, 136, 171, 168, 68, 175, 74, 165, 71, 134, 139, 48, 27, 166, 77, 146, 158, 231, 83, 111, 229, 122, 60, 211, 133, 230, 220, 105, 92, 41, + 55, 46, 245, 40, 244, 102, 143, 54, 65, 25, 63, 161, 1, 216, 80, 73, 209, 76, 132, 187, 208, 89, 18, 169, 200, 196, 135, 130, 116, 188, 159, 86, + 164, 100, 109, 198, 173, 186, 3, 64, 52, 217, 226, 250, 124, 123, 5, 202, 38, 147, 118, 126, 255, 82, 85, 212, 207, 206, 59, 227, 47, 16, 58, 17, + 182, 189, 28, 42, 223, 183, 170, 213, 119, 248, 152, 2, 44, 154, 163, 70, 221, 153, 101, 155, 167, 43, 172, 9, 129, 22, 39, 253, 19, 98, 108, 110, + 79, 113, 224, 232, 178, 185, 112, 104, 218, 246, 97, 228, 251, 34, 242, 193, 238, 210, 144, 12, 191, 179, 162, 241, 81, 51, 145, 235, 249, 14, + 239, 107, 49, 192, 214, 31, 181, 199, 106, 157, 184, 84, 204, 176, 115, 121, 50, 45, 127, 4, 150, 254, 138, 236, 205, 93, 222, 114, 67, 29, 24, + 72, 243, 141, 128, 195, 78, 66, 215, 61, 156, 180, 151}; + +const float kPerlinGrad[16][3] = {{1, 1, 0}, {-1, 1, 0}, {1, -1, 0}, {-1, -1, 0}, {1, 0, 1}, {-1, 0, 1}, {1, 0, -1}, {-1, 0, -1}, {0, 1, 1}, + {0, -1, 1}, {0, 1, -1}, {0, -1, -1}, {1, 1, 0}, {0, -1, 1}, {-1, 1, 0}, {0, -1, -1}}; + +static float perlin_fade(float t) { return t * t * t * (t * (t * 6 - 15) + 10); } -static float math_lerp(float t, float a, float b) +static float perlin_lerp(float t, float a, float b) { return a + t * (b - a); } static float grad(unsigned char hash, float x, float y, float z) { + LUAU_ASSERT(!FFlag::LuauFasterNoise); unsigned char h = hash & 15; float u = (h < 8) ? x : y; float v = (h < 4) ? y : (h == 12 || h == 14) ? x : z; @@ -314,8 +331,15 @@ static float grad(unsigned char hash, float x, float y, float z) return (h & 1 ? -u : u) + (h & 2 ? -v : v); } -static float perlin(float x, float y, float z) +static float perlin_grad(int hash, float x, float y, float z) { + const float* g = kPerlinGrad[hash & 15]; + return g[0] * x + g[1] * y + g[2] * z; +} + +static float perlin_dep(float x, float y, float z) +{ + LUAU_ASSERT(!FFlag::LuauFasterNoise); float xflr = floorf(x); float yflr = floorf(y); float zflr = floorf(z); @@ -328,9 +352,9 @@ static float perlin(float x, float y, float z) float yf = y - yflr; float zf = z - zflr; - float u = fade(xf); - float v = fade(yf); - float w = fade(zf); + float u = perlin_fade(xf); + float v = perlin_fade(yf); + float w = perlin_fade(zf); const unsigned char* p = kPerlin; @@ -342,24 +366,79 @@ static float perlin(float x, float y, float z) int ba = p[b] + zi; int bb = p[b + 1] + zi; - return math_lerp(w, - math_lerp(v, math_lerp(u, grad(p[aa], xf, yf, zf), grad(p[ba], xf - 1, yf, zf)), - math_lerp(u, grad(p[ab], xf, yf - 1, zf), grad(p[bb], xf - 1, yf - 1, zf))), - math_lerp(v, math_lerp(u, grad(p[aa + 1], xf, yf, zf - 1), grad(p[ba + 1], xf - 1, yf, zf - 1)), - math_lerp(u, grad(p[ab + 1], xf, yf - 1, zf - 1), grad(p[bb + 1], xf - 1, yf - 1, zf - 1)))); + return perlin_lerp(w, + perlin_lerp(v, perlin_lerp(u, grad(p[aa], xf, yf, zf), grad(p[ba], xf - 1, yf, zf)), + perlin_lerp(u, grad(p[ab], xf, yf - 1, zf), grad(p[bb], xf - 1, yf - 1, zf))), + perlin_lerp(v, perlin_lerp(u, grad(p[aa + 1], xf, yf, zf - 1), grad(p[ba + 1], xf - 1, yf, zf - 1)), + perlin_lerp(u, grad(p[ab + 1], xf, yf - 1, zf - 1), grad(p[bb + 1], xf - 1, yf - 1, zf - 1)))); +} + +static float perlin(float x, float y, float z) +{ + LUAU_ASSERT(FFlag::LuauFasterNoise); + float xflr = floorf(x); + float yflr = floorf(y); + float zflr = floorf(z); + + int xi = int(xflr) & 255; + int yi = int(yflr) & 255; + int zi = int(zflr) & 255; + + float xf = x - xflr; + float yf = y - yflr; + float zf = z - zflr; + + float u = perlin_fade(xf); + float v = perlin_fade(yf); + float w = perlin_fade(zf); + + const unsigned char* p = kPerlinHash; + + int a = (p[xi] + yi) & 255; + int aa = (p[a] + zi) & 255; + int ab = (p[a + 1] + zi) & 255; + + int b = (p[xi + 1] + yi) & 255; + int ba = (p[b] + zi) & 255; + int bb = (p[b + 1] + zi) & 255; + + float la = perlin_lerp(u, perlin_grad(p[aa], xf, yf, zf), perlin_grad(p[ba], xf - 1, yf, zf)); + float lb = perlin_lerp(u, perlin_grad(p[ab], xf, yf - 1, zf), perlin_grad(p[bb], xf - 1, yf - 1, zf)); + float la1 = perlin_lerp(u, perlin_grad(p[aa + 1], xf, yf, zf - 1), perlin_grad(p[ba + 1], xf - 1, yf, zf - 1)); + float lb1 = perlin_lerp(u, perlin_grad(p[ab + 1], xf, yf - 1, zf - 1), perlin_grad(p[bb + 1], xf - 1, yf - 1, zf - 1)); + + return perlin_lerp(w, perlin_lerp(v, la, lb), perlin_lerp(v, la1, lb1)); } static int math_noise(lua_State* L) { - double x = luaL_checknumber(L, 1); - double y = luaL_optnumber(L, 2, 0.0); - double z = luaL_optnumber(L, 3, 0.0); + if (FFlag::LuauFasterNoise) + { + int nx, ny, nz; + double x = lua_tonumberx(L, 1, &nx); + double y = lua_tonumberx(L, 2, &ny); + double z = lua_tonumberx(L, 3, &nz); - double r = perlin((float)x, (float)y, (float)z); + luaL_argexpected(L, nx, 1, "number"); + luaL_argexpected(L, ny || lua_isnoneornil(L, 2), 2, "number"); + luaL_argexpected(L, nz || lua_isnoneornil(L, 3), 3, "number"); - lua_pushnumber(L, r); + double r = perlin((float)x, (float)y, (float)z); - return 1; + lua_pushnumber(L, r); + return 1; + } + else + { + double x = luaL_checknumber(L, 1); + double y = luaL_optnumber(L, 2, 0.0); + double z = luaL_optnumber(L, 3, 0.0); + + double r = perlin_dep((float)x, (float)y, (float)z); + + lua_pushnumber(L, r); + return 1; + } } static int math_clamp(lua_State* L) diff --git a/VM/src/lobject.h b/VM/src/lobject.h index 74ea1623..18ff7546 100644 --- a/VM/src/lobject.h +++ b/VM/src/lobject.h @@ -290,6 +290,8 @@ typedef struct Proto uint8_t* typeinfo; + void* userdata; + GCObject* gclist; diff --git a/VM/src/ltablib.cpp b/VM/src/ltablib.cpp index 9bc624e9..fbf03deb 100644 --- a/VM/src/ltablib.cpp +++ b/VM/src/ltablib.cpp @@ -10,6 +10,8 @@ #include "ldebug.h" #include "lvm.h" +LUAU_FASTFLAGVARIABLE(LuauFasterTableConcat, false) + static int foreachi(lua_State* L) { luaL_checktype(L, 1, LUA_TTABLE); @@ -219,8 +221,8 @@ static int tmove(lua_State* L) static void addfield(lua_State* L, luaL_Buffer* b, int i) { - lua_rawgeti(L, 1, i); - if (!lua_isstring(L, -1)) + int tt = lua_rawgeti(L, 1, i); + if (FFlag::LuauFasterTableConcat ? (tt != LUA_TSTRING && tt != LUA_TNUMBER) : !lua_isstring(L, -1)) luaL_error(L, "invalid value (%s) at index %d in table for 'concat'", luaL_typename(L, -1), i); luaL_addvalue(b); } diff --git a/VM/src/lvmexecute.cpp b/VM/src/lvmexecute.cpp index 90c5a7e8..2909d477 100644 --- a/VM/src/lvmexecute.cpp +++ b/VM/src/lvmexecute.cpp @@ -131,6 +131,9 @@ goto dispatchContinue #endif +// Does VM support native execution via ExecutionCallbacks? We mostly assume it does but keep the define to make it easy to quantify the cost. +#define VM_HAS_NATIVE LUA_CUSTOM_EXECUTION + LUAU_NOINLINE void luau_callhook(lua_State* L, lua_Hook hook, void* userdata) { ptrdiff_t base = savestack(L, L->base); @@ -207,7 +210,7 @@ static void luau_execute(lua_State* L) LUAU_ASSERT(L->isactive); LUAU_ASSERT(!isblack(obj2gco(L))); // we don't use luaC_threadbarrier because active threads never turn black -#if LUA_CUSTOM_EXECUTION +#if VM_HAS_NATIVE if ((L->ci->flags & LUA_CALLINFO_NATIVE) && !SingleStep) { Proto* p = clvalue(L->ci->func)->l.p; @@ -1036,7 +1039,7 @@ reentry: Closure* nextcl = clvalue(cip->func); Proto* nextproto = nextcl->l.p; -#if LUA_CUSTOM_EXECUTION +#if VM_HAS_NATIVE if (LUAU_UNLIKELY((cip->flags & LUA_CALLINFO_NATIVE) && !SingleStep)) { if (L->global->ecb.enter(L, nextproto) == 1) @@ -2371,7 +2374,7 @@ reentry: ci->flags = LUA_CALLINFO_NATIVE; ci->savedpc = p->code; -#if LUA_CUSTOM_EXECUTION +#if VM_HAS_NATIVE if (L->global->ecb.enter(L, p) == 1) goto reentry; else @@ -2890,7 +2893,7 @@ int luau_precall(lua_State* L, StkId func, int nresults) ci->savedpc = p->code; -#if LUA_CUSTOM_EXECUTION +#if VM_HAS_NATIVE if (p->execdata) ci->flags = LUA_CALLINFO_NATIVE; #endif diff --git a/bench/micro_tests/test_ToNumberString.lua b/bench/micro_tests/test_ToNumberString.lua new file mode 100644 index 00000000..61104783 --- /dev/null +++ b/bench/micro_tests/test_ToNumberString.lua @@ -0,0 +1,22 @@ +local bench = script and require(script.Parent.bench_support) or require("bench_support") + +bench.runCode(function() + for j=1,1e6 do + tonumber("42") + tonumber(42) + end +end, "tonumber") + +bench.runCode(function() + for j=1,1e6 do + tostring(nil) + tostring("test") + tostring(42) + end +end, "tostring") + +bench.runCode(function() + for j=1,1e6 do + tostring(j) + end +end, "tostring-gc") diff --git a/tests/Autocomplete.test.cpp b/tests/Autocomplete.test.cpp index b8dee997..e13e203a 100644 --- a/tests/Autocomplete.test.cpp +++ b/tests/Autocomplete.test.cpp @@ -15,6 +15,7 @@ LUAU_FASTFLAG(LuauTraceTypesInNonstrictMode2) LUAU_FASTFLAG(LuauSetMetatableDoesNotTimeTravel) +LUAU_FASTFLAG(LuauAutocompleteLastTypecheck) using namespace Luau; @@ -33,14 +34,40 @@ struct ACFixtureImpl : BaseType AutocompleteResult autocomplete(unsigned row, unsigned column) { + if (FFlag::LuauAutocompleteLastTypecheck) + { + FrontendOptions opts; + opts.forAutocomplete = true; + this->frontend.check("MainModule", opts); + } + return Luau::autocomplete(this->frontend, "MainModule", Position{row, column}, nullCallback); } AutocompleteResult autocomplete(char marker, StringCompletionCallback callback = nullCallback) { + if (FFlag::LuauAutocompleteLastTypecheck) + { + FrontendOptions opts; + opts.forAutocomplete = true; + this->frontend.check("MainModule", opts); + } + return Luau::autocomplete(this->frontend, "MainModule", getPosition(marker), callback); } + AutocompleteResult autocomplete(const ModuleName& name, Position pos, StringCompletionCallback callback = nullCallback) + { + if (FFlag::LuauAutocompleteLastTypecheck) + { + FrontendOptions opts; + opts.forAutocomplete = true; + this->frontend.check(name, opts); + } + + return Luau::autocomplete(this->frontend, name, pos, callback); + } + CheckResult check(const std::string& source) { markerPosition.clear(); @@ -99,7 +126,7 @@ struct ACFixtureImpl : BaseType LUAU_ASSERT(i != markerPosition.end()); return i->second; } - + ScopedFastFlag flag{"LuauAutocompleteHideSelfArg", true}; // Maps a marker character (0-9 inclusive) to a position in the source code. std::map markerPosition; }; @@ -1319,7 +1346,7 @@ local a: aa frontend.check("Module/B"); - auto ac = Luau::autocomplete(frontend, "Module/B", Position{2, 11}, nullCallback); + auto ac = autocomplete("Module/B", Position{2, 11}); CHECK(ac.entryMap.count("aaa")); CHECK_EQ(ac.context, AutocompleteContext::Type); @@ -1342,7 +1369,7 @@ local a: aaa. frontend.check("Module/B"); - auto ac = Luau::autocomplete(frontend, "Module/B", Position{2, 13}, nullCallback); + auto ac = autocomplete("Module/B", Position{2, 13}); CHECK_EQ(2, ac.entryMap.size()); CHECK(ac.entryMap.count("A")); @@ -1999,7 +2026,7 @@ ex.a(function(x: frontend.check("Module/B"); - auto ac = Luau::autocomplete(frontend, "Module/B", Position{2, 16}, nullCallback); + auto ac = autocomplete("Module/B", Position{2, 16}); CHECK(!ac.entryMap.count("done")); @@ -2010,7 +2037,7 @@ ex.b(function(x: frontend.check("Module/C"); - ac = Luau::autocomplete(frontend, "Module/C", Position{2, 16}, nullCallback); + ac = autocomplete("Module/C", Position{2, 16}); CHECK(!ac.entryMap.count("(done) -> number")); } @@ -2033,7 +2060,7 @@ ex.a(function(x: frontend.check("Module/B"); - auto ac = Luau::autocomplete(frontend, "Module/B", Position{2, 16}, nullCallback); + auto ac = autocomplete("Module/B", Position{2, 16}); CHECK(!ac.entryMap.count("done")); CHECK(ac.entryMap.count("ex.done")); @@ -2046,7 +2073,7 @@ ex.b(function(x: frontend.check("Module/C"); - ac = Luau::autocomplete(frontend, "Module/C", Position{2, 16}, nullCallback); + ac = autocomplete("Module/C", Position{2, 16}); CHECK(!ac.entryMap.count("(done) -> number")); CHECK(ac.entryMap.count("(ex.done) -> number")); @@ -2360,7 +2387,7 @@ local a: aaa.do frontend.check("Module/B"); - auto ac = Luau::autocomplete(frontend, "Module/B", Position{2, 15}, nullCallback); + auto ac = autocomplete("Module/B", Position{2, 15}); CHECK_EQ(2, ac.entryMap.size()); CHECK(ac.entryMap.count("done")); @@ -2372,7 +2399,7 @@ TEST_CASE_FIXTURE(ACFixture, "comments") { fileResolver.source["Comments"] = "--!str"; - auto ac = Luau::autocomplete(frontend, "Comments", Position{0, 6}, nullCallback); + auto ac = autocomplete("Comments", Position{0, 6}); CHECK_EQ(0, ac.entryMap.size()); } @@ -2391,7 +2418,7 @@ TEST_CASE_FIXTURE(ACBuiltinsFixture, "autocompleteProp_index_function_metamethod -- | Column 20 )"; - auto ac = Luau::autocomplete(frontend, "Module/A", Position{9, 20}, nullCallback); + auto ac = autocomplete("Module/A", Position{9, 20}); REQUIRE_EQ(1, ac.entryMap.size()); CHECK(ac.entryMap.count("x")); } @@ -2484,7 +2511,7 @@ TEST_CASE_FIXTURE(ACFixture, "not_the_var_we_are_defining") { fileResolver.source["Module/A"] = "abc,de"; - auto ac = Luau::autocomplete(frontend, "Module/A", Position{0, 6}, nullCallback); + auto ac = autocomplete("Module/A", Position{0, 6}); CHECK(!ac.entryMap.count("de")); } @@ -2495,7 +2522,7 @@ TEST_CASE_FIXTURE(ACFixture, "recursive_function_global") end )"; - auto ac = Luau::autocomplete(frontend, "global", Position{1, 0}, nullCallback); + auto ac = autocomplete("global", Position{1, 0}); CHECK(ac.entryMap.count("abc")); } @@ -2508,7 +2535,7 @@ TEST_CASE_FIXTURE(ACFixture, "recursive_function_local") end )"; - auto ac = Luau::autocomplete(frontend, "local", Position{1, 0}, nullCallback); + auto ac = autocomplete("local", Position{1, 0}); CHECK(ac.entryMap.count("abc")); } @@ -3147,6 +3174,8 @@ t:@1 REQUIRE(ac.entryMap.count("two")); CHECK(!ac.entryMap["one"].wrongIndexType); CHECK(ac.entryMap["two"].wrongIndexType); + CHECK(ac.entryMap["one"].indexedWithSelf); + CHECK(ac.entryMap["two"].indexedWithSelf); } { @@ -3161,6 +3190,8 @@ t.@1 REQUIRE(ac.entryMap.count("two")); CHECK(ac.entryMap["one"].wrongIndexType); CHECK(!ac.entryMap["two"].wrongIndexType); + CHECK(!ac.entryMap["one"].indexedWithSelf); + CHECK(!ac.entryMap["two"].indexedWithSelf); } } @@ -3190,6 +3221,7 @@ t:@1 REQUIRE(ac.entryMap.count("m")); CHECK(!ac.entryMap["m"].wrongIndexType); + CHECK(ac.entryMap["m"].indexedWithSelf); } TEST_CASE_FIXTURE(ACFixture, "no_incompatible_self_calls") @@ -3204,6 +3236,7 @@ t:@1 REQUIRE(ac.entryMap.count("m")); CHECK(ac.entryMap["m"].wrongIndexType); + CHECK(ac.entryMap["m"].indexedWithSelf); } TEST_CASE_FIXTURE(ACFixture, "no_incompatible_self_calls_2") @@ -3219,6 +3252,7 @@ t:@1 REQUIRE(ac.entryMap.count("f")); CHECK(ac.entryMap["f"].wrongIndexType); + CHECK(ac.entryMap["f"].indexedWithSelf); } TEST_CASE_FIXTURE(ACFixture, "do_wrong_compatible_self_calls") @@ -3234,6 +3268,22 @@ t:@1 REQUIRE(ac.entryMap.count("m")); // We can make changes to mark this as a wrong way to call even though it's compatible CHECK(!ac.entryMap["m"].wrongIndexType); + CHECK(ac.entryMap["m"].indexedWithSelf); +} + +TEST_CASE_FIXTURE(ACFixture, "do_wrong_compatible_nonself_calls") +{ + check(R"( +local t = {} +function t:m(x: string) end +t.@1 + )"); + + auto ac = autocomplete('1'); + + REQUIRE(ac.entryMap.count("m")); + CHECK(!ac.entryMap["m"].wrongIndexType); + CHECK(!ac.entryMap["m"].indexedWithSelf); } TEST_CASE_FIXTURE(ACFixture, "no_wrong_compatible_self_calls_with_generics") @@ -3249,6 +3299,7 @@ t:@1 REQUIRE(ac.entryMap.count("m")); // While this call is compatible with the type, this requires instantiation of a generic type which we don't perform CHECK(ac.entryMap["m"].wrongIndexType); + CHECK(ac.entryMap["m"].indexedWithSelf); } TEST_CASE_FIXTURE(ACFixture, "string_prim_self_calls_are_fine") @@ -3262,10 +3313,13 @@ s:@1 REQUIRE(ac.entryMap.count("byte")); CHECK(ac.entryMap["byte"].wrongIndexType == false); + CHECK(ac.entryMap["byte"].indexedWithSelf); REQUIRE(ac.entryMap.count("char")); CHECK(ac.entryMap["char"].wrongIndexType == true); + CHECK(ac.entryMap["char"].indexedWithSelf); REQUIRE(ac.entryMap.count("sub")); CHECK(ac.entryMap["sub"].wrongIndexType == false); + CHECK(ac.entryMap["sub"].indexedWithSelf); } TEST_CASE_FIXTURE(ACFixture, "string_prim_non_self_calls_are_avoided") @@ -3279,8 +3333,10 @@ s.@1 REQUIRE(ac.entryMap.count("char")); CHECK(ac.entryMap["char"].wrongIndexType == false); + CHECK(!ac.entryMap["char"].indexedWithSelf); REQUIRE(ac.entryMap.count("sub")); CHECK(ac.entryMap["sub"].wrongIndexType == true); + CHECK(!ac.entryMap["sub"].indexedWithSelf); } TEST_CASE_FIXTURE(ACBuiltinsFixture, "library_non_self_calls_are_fine") @@ -3293,10 +3349,13 @@ string.@1 REQUIRE(ac.entryMap.count("byte")); CHECK(ac.entryMap["byte"].wrongIndexType == false); + CHECK(!ac.entryMap["byte"].indexedWithSelf); REQUIRE(ac.entryMap.count("char")); CHECK(ac.entryMap["char"].wrongIndexType == false); + CHECK(!ac.entryMap["char"].indexedWithSelf); REQUIRE(ac.entryMap.count("sub")); CHECK(ac.entryMap["sub"].wrongIndexType == false); + CHECK(!ac.entryMap["sub"].indexedWithSelf); check(R"( table.@1 @@ -3306,10 +3365,13 @@ table.@1 REQUIRE(ac.entryMap.count("remove")); CHECK(ac.entryMap["remove"].wrongIndexType == false); + CHECK(!ac.entryMap["remove"].indexedWithSelf); REQUIRE(ac.entryMap.count("getn")); CHECK(ac.entryMap["getn"].wrongIndexType == false); + CHECK(!ac.entryMap["getn"].indexedWithSelf); REQUIRE(ac.entryMap.count("insert")); CHECK(ac.entryMap["insert"].wrongIndexType == false); + CHECK(!ac.entryMap["insert"].indexedWithSelf); } TEST_CASE_FIXTURE(ACBuiltinsFixture, "library_self_calls_are_invalid") @@ -3322,13 +3384,16 @@ string:@1 REQUIRE(ac.entryMap.count("byte")); CHECK(ac.entryMap["byte"].wrongIndexType == true); + CHECK(ac.entryMap["byte"].indexedWithSelf); REQUIRE(ac.entryMap.count("char")); CHECK(ac.entryMap["char"].wrongIndexType == true); + CHECK(ac.entryMap["char"].indexedWithSelf); // We want the next test to evaluate to 'true', but we have to allow function defined with 'self' to be callable with ':' // We may change the definition of the string metatable to not use 'self' types in the future (like byte/char/pack/unpack) REQUIRE(ac.entryMap.count("sub")); CHECK(ac.entryMap["sub"].wrongIndexType == false); + CHECK(ac.entryMap["sub"].indexedWithSelf); } TEST_CASE_FIXTURE(ACFixture, "source_module_preservation_and_invalidation") @@ -3489,4 +3554,480 @@ TEST_CASE_FIXTURE(ACFixture, "frontend_use_correct_global_scope") CHECK(ac.entryMap.count("Name")); } +TEST_CASE_FIXTURE(ACFixture, "string_completion_outside_quotes") +{ + ScopedFastFlag flag{"LuauDisableCompletionOutsideQuotes", true}; + + loadDefinition(R"( + declare function require(path: string): any + )"); + + std::optional require = frontend.globalsForAutocomplete.globalScope->linearSearchForBinding("require"); + REQUIRE(require); + Luau::unfreeze(frontend.globalsForAutocomplete.globalTypes); + attachTag(require->typeId, "RequireCall"); + Luau::freeze(frontend.globalsForAutocomplete.globalTypes); + + check(R"( + local x = require(@1"@2"@3) + )"); + + StringCompletionCallback callback = [](std::string, std::optional, + std::optional contents) -> std::optional + { + Luau::AutocompleteEntryMap results = {{"test", Luau::AutocompleteEntry{Luau::AutocompleteEntryKind::String, std::nullopt, false, false}}}; + return results; + }; + + auto ac = autocomplete('2', callback); + + CHECK_EQ(1, ac.entryMap.size()); + CHECK(ac.entryMap.count("test")); + + ac = autocomplete('1', callback); + + CHECK_EQ(0, ac.entryMap.size()); + + ac = autocomplete('3', callback); + + CHECK_EQ(0, ac.entryMap.size()); +} + +TEST_CASE_FIXTURE(ACFixture, "anonymous_autofilled_empty") +{ + ScopedFastFlag flag{"LuauAnonymousAutofilled", true}; + + check(R"( +local function foo(a: () -> ()) + a() +end + +foo(@1) + )"); + + const std::optional EXPECTED_INSERT = "function() end"; + + auto ac = autocomplete('1'); + + REQUIRE(ac.entryMap.count(kGeneratedAnonymousFunctionEntryName) == 1); + CHECK(ac.entryMap[kGeneratedAnonymousFunctionEntryName].kind == Luau::AutocompleteEntryKind::GeneratedFunction); + CHECK(ac.entryMap[kGeneratedAnonymousFunctionEntryName].typeCorrect == Luau::TypeCorrectKind::Correct); + REQUIRE(ac.entryMap[kGeneratedAnonymousFunctionEntryName].insertText); + CHECK_EQ(EXPECTED_INSERT, *ac.entryMap[kGeneratedAnonymousFunctionEntryName].insertText); +} + +TEST_CASE_FIXTURE(ACFixture, "anonymous_autofilled_args") +{ + ScopedFastFlag flag{"LuauAnonymousAutofilled", true}; + + check(R"( +local function foo(a: (number, string) -> ()) + a() +end + +foo(@1) + )"); + + const std::optional EXPECTED_INSERT = "function(a0: number, a1: string) end"; + + auto ac = autocomplete('1'); + + REQUIRE(ac.entryMap.count(kGeneratedAnonymousFunctionEntryName) == 1); + CHECK(ac.entryMap[kGeneratedAnonymousFunctionEntryName].kind == Luau::AutocompleteEntryKind::GeneratedFunction); + CHECK(ac.entryMap[kGeneratedAnonymousFunctionEntryName].typeCorrect == Luau::TypeCorrectKind::Correct); + REQUIRE(ac.entryMap[kGeneratedAnonymousFunctionEntryName].insertText); + CHECK_EQ(EXPECTED_INSERT, *ac.entryMap[kGeneratedAnonymousFunctionEntryName].insertText); +} + +TEST_CASE_FIXTURE(ACFixture, "anonymous_autofilled_args_single_return") +{ + ScopedFastFlag flag{"LuauAnonymousAutofilled", true}; + + check(R"( +local function foo(a: (number, string) -> (string)) + a() +end + +foo(@1) + )"); + + const std::optional EXPECTED_INSERT = "function(a0: number, a1: string): string end"; + + auto ac = autocomplete('1'); + + REQUIRE(ac.entryMap.count(kGeneratedAnonymousFunctionEntryName) == 1); + CHECK(ac.entryMap[kGeneratedAnonymousFunctionEntryName].kind == Luau::AutocompleteEntryKind::GeneratedFunction); + CHECK(ac.entryMap[kGeneratedAnonymousFunctionEntryName].typeCorrect == Luau::TypeCorrectKind::Correct); + REQUIRE(ac.entryMap[kGeneratedAnonymousFunctionEntryName].insertText); + CHECK_EQ(EXPECTED_INSERT, *ac.entryMap[kGeneratedAnonymousFunctionEntryName].insertText); +} + +TEST_CASE_FIXTURE(ACFixture, "anonymous_autofilled_args_multi_return") +{ + ScopedFastFlag flag{"LuauAnonymousAutofilled", true}; + + check(R"( +local function foo(a: (number, string) -> (string, number)) + a() +end + +foo(@1) + )"); + + const std::optional EXPECTED_INSERT = "function(a0: number, a1: string): (string, number) end"; + + auto ac = autocomplete('1'); + + REQUIRE(ac.entryMap.count(kGeneratedAnonymousFunctionEntryName) == 1); + CHECK(ac.entryMap[kGeneratedAnonymousFunctionEntryName].kind == Luau::AutocompleteEntryKind::GeneratedFunction); + CHECK(ac.entryMap[kGeneratedAnonymousFunctionEntryName].typeCorrect == Luau::TypeCorrectKind::Correct); + REQUIRE(ac.entryMap[kGeneratedAnonymousFunctionEntryName].insertText); + CHECK_EQ(EXPECTED_INSERT, *ac.entryMap[kGeneratedAnonymousFunctionEntryName].insertText); +} + +TEST_CASE_FIXTURE(ACFixture, "anonymous_autofilled__noargs_multi_return") +{ + ScopedFastFlag flag{"LuauAnonymousAutofilled", true}; + + check(R"( +local function foo(a: () -> (string, number)) + a() +end + +foo(@1) + )"); + + const std::optional EXPECTED_INSERT = "function(): (string, number) end"; + + auto ac = autocomplete('1'); + + REQUIRE(ac.entryMap.count(kGeneratedAnonymousFunctionEntryName) == 1); + CHECK(ac.entryMap[kGeneratedAnonymousFunctionEntryName].kind == Luau::AutocompleteEntryKind::GeneratedFunction); + CHECK(ac.entryMap[kGeneratedAnonymousFunctionEntryName].typeCorrect == Luau::TypeCorrectKind::Correct); + REQUIRE(ac.entryMap[kGeneratedAnonymousFunctionEntryName].insertText); + CHECK_EQ(EXPECTED_INSERT, *ac.entryMap[kGeneratedAnonymousFunctionEntryName].insertText); +} + +TEST_CASE_FIXTURE(ACFixture, "anonymous_autofilled__varargs_multi_return") +{ + ScopedFastFlag flag{"LuauAnonymousAutofilled", true}; + + check(R"( +local function foo(a: (...number) -> (string, number)) + a() +end + +foo(@1) + )"); + + const std::optional EXPECTED_INSERT = "function(...: number): (string, number) end"; + + auto ac = autocomplete('1'); + + REQUIRE(ac.entryMap.count(kGeneratedAnonymousFunctionEntryName) == 1); + CHECK(ac.entryMap[kGeneratedAnonymousFunctionEntryName].kind == Luau::AutocompleteEntryKind::GeneratedFunction); + CHECK(ac.entryMap[kGeneratedAnonymousFunctionEntryName].typeCorrect == Luau::TypeCorrectKind::Correct); + REQUIRE(ac.entryMap[kGeneratedAnonymousFunctionEntryName].insertText); + CHECK_EQ(EXPECTED_INSERT, *ac.entryMap[kGeneratedAnonymousFunctionEntryName].insertText); +} + +TEST_CASE_FIXTURE(ACFixture, "anonymous_autofilled_multi_varargs_multi_return") +{ + ScopedFastFlag flag{"LuauAnonymousAutofilled", true}; + + check(R"( +local function foo(a: (string, ...number) -> (string, number)) + a() +end + +foo(@1) + )"); + + const std::optional EXPECTED_INSERT = "function(a0: string, ...: number): (string, number) end"; + + auto ac = autocomplete('1'); + + REQUIRE(ac.entryMap.count(kGeneratedAnonymousFunctionEntryName) == 1); + CHECK(ac.entryMap[kGeneratedAnonymousFunctionEntryName].kind == Luau::AutocompleteEntryKind::GeneratedFunction); + CHECK(ac.entryMap[kGeneratedAnonymousFunctionEntryName].typeCorrect == Luau::TypeCorrectKind::Correct); + REQUIRE(ac.entryMap[kGeneratedAnonymousFunctionEntryName].insertText); + CHECK_EQ(EXPECTED_INSERT, *ac.entryMap[kGeneratedAnonymousFunctionEntryName].insertText); +} + +TEST_CASE_FIXTURE(ACFixture, "anonymous_autofilled_multi_varargs_varargs_return") +{ + ScopedFastFlag flag{"LuauAnonymousAutofilled", true}; + + check(R"( +local function foo(a: (string, ...number) -> ...number) + a() +end + +foo(@1) + )"); + + const std::optional EXPECTED_INSERT = "function(a0: string, ...: number): ...number end"; + + auto ac = autocomplete('1'); + + REQUIRE(ac.entryMap.count(kGeneratedAnonymousFunctionEntryName) == 1); + CHECK(ac.entryMap[kGeneratedAnonymousFunctionEntryName].kind == Luau::AutocompleteEntryKind::GeneratedFunction); + CHECK(ac.entryMap[kGeneratedAnonymousFunctionEntryName].typeCorrect == Luau::TypeCorrectKind::Correct); + REQUIRE(ac.entryMap[kGeneratedAnonymousFunctionEntryName].insertText); + CHECK_EQ(EXPECTED_INSERT, *ac.entryMap[kGeneratedAnonymousFunctionEntryName].insertText); +} + +TEST_CASE_FIXTURE(ACFixture, "anonymous_autofilled_multi_varargs_multi_varargs_return") +{ + ScopedFastFlag flag{"LuauAnonymousAutofilled", true}; + + check(R"( +local function foo(a: (string, ...number) -> (boolean, ...number)) + a() +end + +foo(@1) + )"); + + const std::optional EXPECTED_INSERT = "function(a0: string, ...: number): (boolean, ...number) end"; + + auto ac = autocomplete('1'); + + REQUIRE(ac.entryMap.count(kGeneratedAnonymousFunctionEntryName) == 1); + CHECK(ac.entryMap[kGeneratedAnonymousFunctionEntryName].kind == Luau::AutocompleteEntryKind::GeneratedFunction); + CHECK(ac.entryMap[kGeneratedAnonymousFunctionEntryName].typeCorrect == Luau::TypeCorrectKind::Correct); + REQUIRE(ac.entryMap[kGeneratedAnonymousFunctionEntryName].insertText); + CHECK_EQ(EXPECTED_INSERT, *ac.entryMap[kGeneratedAnonymousFunctionEntryName].insertText); +} + +TEST_CASE_FIXTURE(ACFixture, "anonymous_autofilled_named_args") +{ + ScopedFastFlag flag{"LuauAnonymousAutofilled", true}; + + check(R"( +local function foo(a: (foo: number, bar: string) -> (string, number)) + a() +end + +foo(@1) + )"); + + const std::optional EXPECTED_INSERT = "function(foo: number, bar: string): (string, number) end"; + + auto ac = autocomplete('1'); + + REQUIRE(ac.entryMap.count(kGeneratedAnonymousFunctionEntryName) == 1); + CHECK(ac.entryMap[kGeneratedAnonymousFunctionEntryName].kind == Luau::AutocompleteEntryKind::GeneratedFunction); + CHECK(ac.entryMap[kGeneratedAnonymousFunctionEntryName].typeCorrect == Luau::TypeCorrectKind::Correct); + REQUIRE(ac.entryMap[kGeneratedAnonymousFunctionEntryName].insertText); + CHECK_EQ(EXPECTED_INSERT, *ac.entryMap[kGeneratedAnonymousFunctionEntryName].insertText); +} + +TEST_CASE_FIXTURE(ACFixture, "anonymous_autofilled_partially_args") +{ + ScopedFastFlag flag{"LuauAnonymousAutofilled", true}; + + check(R"( +local function foo(a: (number, bar: string) -> (string, number)) + a() +end + +foo(@1) + )"); + + const std::optional EXPECTED_INSERT = "function(a0: number, bar: string): (string, number) end"; + + auto ac = autocomplete('1'); + + REQUIRE(ac.entryMap.count(kGeneratedAnonymousFunctionEntryName) == 1); + CHECK(ac.entryMap[kGeneratedAnonymousFunctionEntryName].kind == Luau::AutocompleteEntryKind::GeneratedFunction); + CHECK(ac.entryMap[kGeneratedAnonymousFunctionEntryName].typeCorrect == Luau::TypeCorrectKind::Correct); + REQUIRE(ac.entryMap[kGeneratedAnonymousFunctionEntryName].insertText); + CHECK_EQ(EXPECTED_INSERT, *ac.entryMap[kGeneratedAnonymousFunctionEntryName].insertText); +} + +TEST_CASE_FIXTURE(ACFixture, "anonymous_autofilled_partially_args_last") +{ + ScopedFastFlag flag{"LuauAnonymousAutofilled", true}; + + check(R"( +local function foo(a: (foo: number, string) -> (string, number)) + a() +end + +foo(@1) + )"); + + const std::optional EXPECTED_INSERT = "function(foo: number, a1: string): (string, number) end"; + + auto ac = autocomplete('1'); + + REQUIRE(ac.entryMap.count(kGeneratedAnonymousFunctionEntryName) == 1); + CHECK(ac.entryMap[kGeneratedAnonymousFunctionEntryName].kind == Luau::AutocompleteEntryKind::GeneratedFunction); + CHECK(ac.entryMap[kGeneratedAnonymousFunctionEntryName].typeCorrect == Luau::TypeCorrectKind::Correct); + REQUIRE(ac.entryMap[kGeneratedAnonymousFunctionEntryName].insertText); + CHECK_EQ(EXPECTED_INSERT, *ac.entryMap[kGeneratedAnonymousFunctionEntryName].insertText); +} + +TEST_CASE_FIXTURE(ACFixture, "anonymous_autofilled_typeof_args") +{ + ScopedFastFlag flag{"LuauAnonymousAutofilled", true}; + + check(R"( +local t = { a = 1, b = 2 } + +local function foo(a: (foo: typeof(t)) -> ()) + a() +end + +foo(@1) + )"); + + const std::optional EXPECTED_INSERT = "function(foo) end"; // Cannot utter this type. + + auto ac = autocomplete('1'); + + REQUIRE(ac.entryMap.count(kGeneratedAnonymousFunctionEntryName) == 1); + CHECK(ac.entryMap[kGeneratedAnonymousFunctionEntryName].kind == Luau::AutocompleteEntryKind::GeneratedFunction); + CHECK(ac.entryMap[kGeneratedAnonymousFunctionEntryName].typeCorrect == Luau::TypeCorrectKind::Correct); + REQUIRE(ac.entryMap[kGeneratedAnonymousFunctionEntryName].insertText); + CHECK_EQ(EXPECTED_INSERT, *ac.entryMap[kGeneratedAnonymousFunctionEntryName].insertText); +} + +TEST_CASE_FIXTURE(ACFixture, "anonymous_autofilled_table_literal_args") +{ + ScopedFastFlag flag{"LuauAnonymousAutofilled", true}; + + check(R"( +local function foo(a: (tbl: { x: number, y: number }) -> number) return a({x=2, y = 3}) end +foo(@1) + )"); + + const std::optional EXPECTED_INSERT = "function(tbl: { x: number, y: number }): number end"; + + auto ac = autocomplete('1'); + + REQUIRE(ac.entryMap.count(kGeneratedAnonymousFunctionEntryName) == 1); + CHECK(ac.entryMap[kGeneratedAnonymousFunctionEntryName].kind == Luau::AutocompleteEntryKind::GeneratedFunction); + CHECK(ac.entryMap[kGeneratedAnonymousFunctionEntryName].typeCorrect == Luau::TypeCorrectKind::Correct); + REQUIRE(ac.entryMap[kGeneratedAnonymousFunctionEntryName].insertText); + CHECK_EQ(EXPECTED_INSERT, *ac.entryMap[kGeneratedAnonymousFunctionEntryName].insertText); +} + +TEST_CASE_FIXTURE(ACFixture, "anonymous_autofilled_typeof_returns") +{ + ScopedFastFlag flag{"LuauAnonymousAutofilled", true}; + + check(R"( +local t = { a = 1, b = 2 } + +local function foo(a: () -> typeof(t)) + a() +end + +foo(@1) + )"); + + const std::optional EXPECTED_INSERT = "function() end"; // Cannot utter this type. + + auto ac = autocomplete('1'); + + REQUIRE(ac.entryMap.count(kGeneratedAnonymousFunctionEntryName) == 1); + CHECK(ac.entryMap[kGeneratedAnonymousFunctionEntryName].kind == Luau::AutocompleteEntryKind::GeneratedFunction); + CHECK(ac.entryMap[kGeneratedAnonymousFunctionEntryName].typeCorrect == Luau::TypeCorrectKind::Correct); + REQUIRE(ac.entryMap[kGeneratedAnonymousFunctionEntryName].insertText); + CHECK_EQ(EXPECTED_INSERT, *ac.entryMap[kGeneratedAnonymousFunctionEntryName].insertText); +} + +TEST_CASE_FIXTURE(ACFixture, "anonymous_autofilled_table_literal_args") +{ + ScopedFastFlag flag{"LuauAnonymousAutofilled", true}; + + check(R"( +local function foo(a: () -> { x: number, y: number }) return {x=2, y = 3} end +foo(@1) + )"); + + const std::optional EXPECTED_INSERT = "function(): { x: number, y: number } end"; + + auto ac = autocomplete('1'); + + REQUIRE(ac.entryMap.count(kGeneratedAnonymousFunctionEntryName) == 1); + CHECK(ac.entryMap[kGeneratedAnonymousFunctionEntryName].kind == Luau::AutocompleteEntryKind::GeneratedFunction); + CHECK(ac.entryMap[kGeneratedAnonymousFunctionEntryName].typeCorrect == Luau::TypeCorrectKind::Correct); + REQUIRE(ac.entryMap[kGeneratedAnonymousFunctionEntryName].insertText); + CHECK_EQ(EXPECTED_INSERT, *ac.entryMap[kGeneratedAnonymousFunctionEntryName].insertText); +} + +TEST_CASE_FIXTURE(ACFixture, "anonymous_autofilled_typeof_vararg") +{ + ScopedFastFlag flag{"LuauAnonymousAutofilled", true}; + + check(R"( +local t = { a = 1, b = 2 } + +local function foo(a: (...typeof(t)) -> ()) + a() +end + +foo(@1) + )"); + + const std::optional EXPECTED_INSERT = "function(...) end"; // Cannot utter this type. + + auto ac = autocomplete('1'); + + REQUIRE(ac.entryMap.count(kGeneratedAnonymousFunctionEntryName) == 1); + CHECK(ac.entryMap[kGeneratedAnonymousFunctionEntryName].kind == Luau::AutocompleteEntryKind::GeneratedFunction); + CHECK(ac.entryMap[kGeneratedAnonymousFunctionEntryName].typeCorrect == Luau::TypeCorrectKind::Correct); + REQUIRE(ac.entryMap[kGeneratedAnonymousFunctionEntryName].insertText); + CHECK_EQ(EXPECTED_INSERT, *ac.entryMap[kGeneratedAnonymousFunctionEntryName].insertText); +} + +TEST_CASE_FIXTURE(ACFixture, "anonymous_autofilled_generic_type_pack_vararg") +{ + ScopedFastFlag flag{"LuauAnonymousAutofilled", true}; + + check(R"( +local function foo(a: (...A) -> number, ...: A) + return a(...) +end + +foo(@1) + )"); + + const std::optional EXPECTED_INSERT = "function(...): number end"; + + auto ac = autocomplete('1'); + + REQUIRE(ac.entryMap.count(kGeneratedAnonymousFunctionEntryName) == 1); + CHECK(ac.entryMap[kGeneratedAnonymousFunctionEntryName].kind == Luau::AutocompleteEntryKind::GeneratedFunction); + CHECK(ac.entryMap[kGeneratedAnonymousFunctionEntryName].typeCorrect == Luau::TypeCorrectKind::Correct); + REQUIRE(ac.entryMap[kGeneratedAnonymousFunctionEntryName].insertText); + CHECK_EQ(EXPECTED_INSERT, *ac.entryMap[kGeneratedAnonymousFunctionEntryName].insertText); +} + +TEST_CASE_FIXTURE(ACFixture, "anonymous_autofilled_generic_on_argument_type_pack_vararg") +{ + ScopedFastFlag flag{"LuauAnonymousAutofilled", true}; + + check(R"( +local function foo(a: (...: T...) -> number) + return a(4, 5, 6) +end + +foo(@1) + )"); + + const std::optional EXPECTED_INSERT = "function(...): number end"; + + auto ac = autocomplete('1'); + + REQUIRE(ac.entryMap.count(kGeneratedAnonymousFunctionEntryName) == 1); + CHECK(ac.entryMap[kGeneratedAnonymousFunctionEntryName].kind == Luau::AutocompleteEntryKind::GeneratedFunction); + CHECK(ac.entryMap[kGeneratedAnonymousFunctionEntryName].typeCorrect == Luau::TypeCorrectKind::Correct); + REQUIRE(ac.entryMap[kGeneratedAnonymousFunctionEntryName].insertText); + CHECK_EQ(EXPECTED_INSERT, *ac.entryMap[kGeneratedAnonymousFunctionEntryName].insertText); +} + TEST_SUITE_END(); diff --git a/tests/Compiler.test.cpp b/tests/Compiler.test.cpp index db779da2..7abf0423 100644 --- a/tests/Compiler.test.cpp +++ b/tests/Compiler.test.cpp @@ -6978,6 +6978,8 @@ L3: RETURN R0 0 TEST_CASE("BuiltinArity") { + ScopedFastFlag sff("LuauCompileFixBuiltinArity", true); + // by default we can't assume that we know parameter/result count for builtins as they can be overridden at runtime CHECK_EQ("\n" + compileFunction(R"( return math.abs(unknown()) @@ -7037,6 +7039,21 @@ FASTCALL 34 L0 GETIMPORT R0 4 [bit32.extract] CALL R0 -1 1 L0: RETURN R0 1 +)"); + + // some builtins are not variadic and have a fixed number of arguments but are not none-safe, meaning that we can't replace calls that may + // return none with calls that will return nil + CHECK_EQ("\n" + compileFunction(R"( +return type(unknown()) +)", + 0, 2), + R"( +GETIMPORT R1 1 [unknown] +CALL R1 0 -1 +FASTCALL 40 L0 +GETIMPORT R0 3 [type] +CALL R0 -1 1 +L0: RETURN R0 1 )"); // importantly, this optimization also helps us get around the multret inlining restriction for builtin wrappers diff --git a/tests/Conformance.test.cpp b/tests/Conformance.test.cpp index c98dabb9..c07aab0d 100644 --- a/tests/Conformance.test.cpp +++ b/tests/Conformance.test.cpp @@ -273,6 +273,8 @@ TEST_CASE("Assert") TEST_CASE("Basic") { + ScopedFastFlag sff("LuauCompileFixBuiltinArity", true); + runConformance("basic.lua"); } @@ -326,6 +328,8 @@ TEST_CASE("Clear") TEST_CASE("Strings") { + ScopedFastFlag sff("LuauCompileFixBuiltinArity", true); + runConformance("strings.lua"); } @@ -1112,6 +1116,34 @@ static bool endsWith(const std::string& str, const std::string& suffix) return suffix == std::string_view(str.c_str() + str.length() - suffix.length(), suffix.length()); } +TEST_CASE("ApiType") +{ + StateRef globalState(luaL_newstate(), lua_close); + lua_State* L = globalState.get(); + + lua_pushnumber(L, 2); + CHECK(strcmp(luaL_typename(L, -1), "number") == 0); + CHECK(strcmp(luaL_typename(L, 1), "number") == 0); + CHECK(lua_type(L, -1) == LUA_TNUMBER); + CHECK(lua_type(L, 1) == LUA_TNUMBER); + + CHECK(strcmp(luaL_typename(L, 2), "no value") == 0); + CHECK(lua_type(L, 2) == LUA_TNONE); + CHECK(strcmp(lua_typename(L, lua_type(L, 2)), "no value") == 0); + + lua_newuserdata(L, 0); + CHECK(strcmp(luaL_typename(L, -1), "userdata") == 0); + CHECK(lua_type(L, -1) == LUA_TUSERDATA); + + lua_newtable(L); + lua_pushstring(L, "hello"); + lua_setfield(L, -2, "__type"); + lua_setmetatable(L, -2); + + CHECK(strcmp(luaL_typename(L, -1), "hello") == 0); + CHECK(lua_type(L, -1) == LUA_TUSERDATA); +} + #if !LUA_USE_LONGJMP TEST_CASE("ExceptionObject") { diff --git a/tests/ConstraintGraphBuilderFixture.cpp b/tests/ConstraintGraphBuilderFixture.cpp index 6bfb1590..01b9a5dd 100644 --- a/tests/ConstraintGraphBuilderFixture.cpp +++ b/tests/ConstraintGraphBuilderFixture.cpp @@ -21,7 +21,7 @@ void ConstraintGraphBuilderFixture::generateConstraints(const std::string& code) AstStatBlock* root = parse(code); dfg = std::make_unique(DataFlowGraphBuilder::build(root, NotNull{&ice})); cgb = std::make_unique(mainModule, &arena, NotNull(&moduleResolver), builtinTypes, NotNull(&ice), - frontend.globals.globalScope, /*prepareModuleScope*/ nullptr, &logger, NotNull{dfg.get()}); + frontend.globals.globalScope, /*prepareModuleScope*/ nullptr, &logger, NotNull{dfg.get()}, std::vector()); cgb->visit(root); rootScope = cgb->rootScope; constraints = Luau::borrowConstraints(cgb->constraints); @@ -30,7 +30,7 @@ void ConstraintGraphBuilderFixture::generateConstraints(const std::string& code) void ConstraintGraphBuilderFixture::solve(const std::string& code) { generateConstraints(code); - ConstraintSolver cs{NotNull{&normalizer}, NotNull{rootScope}, constraints, "MainModule", NotNull(&moduleResolver), {}, &logger}; + ConstraintSolver cs{NotNull{&normalizer}, NotNull{rootScope}, constraints, "MainModule", NotNull(&moduleResolver), {}, &logger, {}}; cs.run(); } diff --git a/tests/CostModel.test.cpp b/tests/CostModel.test.cpp index 686a99d1..206b83a8 100644 --- a/tests/CostModel.test.cpp +++ b/tests/CostModel.test.cpp @@ -133,6 +133,8 @@ end TEST_CASE("ControlFlow") { + ScopedFastFlag sff("LuauAssignmentHasCost", true); + uint64_t model = modelFunction(R"( function test(a) while a < 0 do @@ -156,8 +158,8 @@ end const bool args1[] = {false}; const bool args2[] = {true}; - CHECK_EQ(82, Luau::Compile::computeCost(model, args1, 1)); - CHECK_EQ(79, Luau::Compile::computeCost(model, args2, 1)); + CHECK_EQ(76, Luau::Compile::computeCost(model, args1, 1)); + CHECK_EQ(73, Luau::Compile::computeCost(model, args2, 1)); } TEST_CASE("Conditional") @@ -240,4 +242,25 @@ end CHECK_EQ(3, Luau::Compile::computeCost(model, args2, 1)); } +TEST_CASE("MultipleAssignments") +{ + ScopedFastFlag sff("LuauAssignmentHasCost", true); + + uint64_t model = modelFunction(R"( +function test(a) + local x = 0 + x = a + x = a + 1 + x, x, x = a + x = a, a, a +end +)"); + + const bool args1[] = {false}; + const bool args2[] = {true}; + + CHECK_EQ(8, Luau::Compile::computeCost(model, args1, 1)); + CHECK_EQ(7, Luau::Compile::computeCost(model, args2, 1)); +} + TEST_SUITE_END(); diff --git a/tests/Differ.test.cpp b/tests/Differ.test.cpp index 1e9dcac3..132b0267 100644 --- a/tests/Differ.test.cpp +++ b/tests/Differ.test.cpp @@ -17,308 +17,266 @@ LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution) TEST_SUITE_BEGIN("Differ"); -TEST_CASE_FIXTURE(Fixture, "equal_numbers") +TEST_CASE_FIXTURE(DifferFixture, "equal_numbers") { CheckResult result = check(R"( local foo = 5 local almostFoo = 78 - almostFoo = foo )"); LUAU_REQUIRE_NO_ERRORS(result); - TypeId foo = requireType("foo"); - TypeId almostFoo = requireType("almostFoo"); - try - { - DifferResult diffRes = diff(foo, almostFoo); - CHECK(!diffRes.diffError.has_value()); - } - catch (const InternalCompilerError& e) - { - INFO(("InternalCompilerError: " + e.message)); - CHECK(false); - } + compareTypesEq("foo", "almostFoo"); } -TEST_CASE_FIXTURE(Fixture, "equal_strings") +TEST_CASE_FIXTURE(DifferFixture, "equal_strings") { CheckResult result = check(R"( local foo = "hello" local almostFoo = "world" - almostFoo = foo )"); LUAU_REQUIRE_NO_ERRORS(result); - TypeId foo = requireType("foo"); - TypeId almostFoo = requireType("almostFoo"); - try - { - DifferResult diffRes = diff(foo, almostFoo); - CHECK(!diffRes.diffError.has_value()); - } - catch (const InternalCompilerError& e) - { - INFO(("InternalCompilerError: " + e.message)); - CHECK(false); - } + compareTypesEq("foo", "almostFoo"); } -TEST_CASE_FIXTURE(Fixture, "equal_tables") +TEST_CASE_FIXTURE(DifferFixture, "equal_tables") { CheckResult result = check(R"( local foo = { x = 1, y = "where" } local almostFoo = { x = 5, y = "when" } - almostFoo = foo )"); LUAU_REQUIRE_NO_ERRORS(result); - TypeId foo = requireType("foo"); - TypeId almostFoo = requireType("almostFoo"); - try - { - DifferResult diffRes = diff(foo, almostFoo); - CHECK(!diffRes.diffError.has_value()); - } - catch (const InternalCompilerError& e) - { - INFO(("InternalCompilerError: " + e.message)); - CHECK(false); - } + compareTypesEq("foo", "almostFoo"); } -TEST_CASE_FIXTURE(Fixture, "a_table_missing_property") +TEST_CASE_FIXTURE(DifferFixture, "a_table_missing_property") { CheckResult result = check(R"( local foo = { x = 1, y = 2 } local almostFoo = { x = 1, z = 3 } - almostFoo = foo )"); - LUAU_REQUIRE_ERRORS(result); + LUAU_REQUIRE_NO_ERRORS(result); - TypeId foo = requireType("foo"); - TypeId almostFoo = requireType("almostFoo"); - std::string diffMessage; - try - { - diffMessage = diff(foo, almostFoo).diffError->toString(); - } - catch (const InternalCompilerError& e) - { - INFO(("InternalCompilerError: " + e.message)); - CHECK(false); - } - CHECK_EQ("DiffError: these two types are not equal because the left type at foo.y has type number, while the right type at almostFoo is missing " - "the property y", - diffMessage); + compareTypesNe("foo", "almostFoo", + "DiffError: these two types are not equal because the left type at foo.y has type number, while the right type at almostFoo is missing " + "the property y"); } -TEST_CASE_FIXTURE(Fixture, "left_table_missing_property") +TEST_CASE_FIXTURE(DifferFixture, "left_table_missing_property") { CheckResult result = check(R"( local foo = { x = 1 } local almostFoo = { x = 1, z = 3 } - almostFoo = foo )"); - LUAU_REQUIRE_ERRORS(result); + LUAU_REQUIRE_NO_ERRORS(result); - TypeId foo = requireType("foo"); - TypeId almostFoo = requireType("almostFoo"); - std::string diffMessage; - try - { - diffMessage = diff(foo, almostFoo).diffError->toString(); - } - catch (const InternalCompilerError& e) - { - INFO(("InternalCompilerError: " + e.message)); - CHECK(false); - } - CHECK_EQ("DiffError: these two types are not equal because the left type at foo is missing the property z, while the right type at almostFoo.z " - "has type number", - diffMessage); + compareTypesNe("foo", "almostFoo", + "DiffError: these two types are not equal because the left type at foo is missing the property z, while the right type at almostFoo.z " + "has type number"); } -TEST_CASE_FIXTURE(Fixture, "a_table_wrong_type") +TEST_CASE_FIXTURE(DifferFixture, "a_table_wrong_type") { CheckResult result = check(R"( local foo = { x = 1, y = 2 } local almostFoo = { x = 1, y = "two" } - almostFoo = foo )"); - LUAU_REQUIRE_ERRORS(result); + LUAU_REQUIRE_NO_ERRORS(result); - TypeId foo = requireType("foo"); - TypeId almostFoo = requireType("almostFoo"); - std::string diffMessage; - try - { - diffMessage = diff(foo, almostFoo).diffError->toString(); - } - catch (const InternalCompilerError& e) - { - INFO(("InternalCompilerError: " + e.message)); - CHECK(false); - } - CHECK_EQ("DiffError: these two types are not equal because the left type at foo.y has type number, while the right type at almostFoo.y has type " - "string", - diffMessage); + compareTypesNe("foo", "almostFoo", + "DiffError: these two types are not equal because the left type at foo.y has type number, while the right type at almostFoo.y has type " + "string"); } -TEST_CASE_FIXTURE(Fixture, "a_table_wrong_type") +TEST_CASE_FIXTURE(DifferFixture, "a_table_wrong_type") { CheckResult result = check(R"( local foo: string local almostFoo: number - almostFoo = foo )"); - LUAU_REQUIRE_ERRORS(result); + LUAU_REQUIRE_NO_ERRORS(result); - TypeId foo = requireType("foo"); - TypeId almostFoo = requireType("almostFoo"); - std::string diffMessage; - try - { - diffMessage = diff(foo, almostFoo).diffError->toString(); - } - catch (const InternalCompilerError& e) - { - INFO(("InternalCompilerError: " + e.message)); - CHECK(false); - } - CHECK_EQ("DiffError: these two types are not equal because the left type at has type string, while the right type at " - " has type number", - diffMessage); + compareTypesNe("foo", "almostFoo", + "DiffError: these two types are not equal because the left type at has type string, while the right type at " + " has type number"); } -TEST_CASE_FIXTURE(Fixture, "a_nested_table_wrong_type") +TEST_CASE_FIXTURE(DifferFixture, "a_nested_table_wrong_type") { CheckResult result = check(R"( local foo = { x = 1, inner = { table = { has = { wrong = { value = 5 } } } } } local almostFoo = { x = 1, inner = { table = { has = { wrong = { value = "five" } } } } } - almostFoo = foo )"); - LUAU_REQUIRE_ERRORS(result); + LUAU_REQUIRE_NO_ERRORS(result); - TypeId foo = requireType("foo"); - TypeId almostFoo = requireType("almostFoo"); - std::string diffMessage; - try - { - diffMessage = diff(foo, almostFoo).diffError->toString(); - } - catch (const InternalCompilerError& e) - { - INFO(("InternalCompilerError: " + e.message)); - CHECK(false); - } - CHECK_EQ("DiffError: these two types are not equal because the left type at foo.inner.table.has.wrong.value has type number, while the right " - "type at almostFoo.inner.table.has.wrong.value has type string", - diffMessage); + compareTypesNe("foo", "almostFoo", + "DiffError: these two types are not equal because the left type at foo.inner.table.has.wrong.value has type number, while the right " + "type at almostFoo.inner.table.has.wrong.value has type string"); } -TEST_CASE_FIXTURE(Fixture, "a_nested_table_wrong_match") +TEST_CASE_FIXTURE(DifferFixture, "a_nested_table_wrong_match") { CheckResult result = check(R"( local foo = { x = 1, inner = { table = { has = { wrong = { variant = { because = { it = { goes = { on = "five" } } } } } } } } } local almostFoo = { x = 1, inner = { table = { has = { wrong = { variant = "five" } } } } } - almostFoo = foo )"); - LUAU_REQUIRE_ERRORS(result); + LUAU_REQUIRE_NO_ERRORS(result); - TypeId foo = requireType("foo"); - TypeId almostFoo = requireType("almostFoo"); - std::string diffMessage; - try - { - diffMessage = diff(foo, almostFoo).diffError->toString(); - } - catch (const InternalCompilerError& e) - { - INFO(("InternalCompilerError: " + e.message)); - CHECK(false); - } - CHECK_EQ("DiffError: these two types are not equal because the left type at foo.inner.table.has.wrong.variant has type { because: { it: { goes: " - "{ on: string } } } }, while the right type at almostFoo.inner.table.has.wrong.variant has type string", - diffMessage); + compareTypesNe("foo", "almostFoo", + "DiffError: these two types are not equal because the left type at foo.inner.table.has.wrong.variant has type { because: { it: { goes: " + "{ on: string } } } }, while the right type at almostFoo.inner.table.has.wrong.variant has type string"); } -TEST_CASE_FIXTURE(Fixture, "singleton") +TEST_CASE_FIXTURE(DifferFixture, "singleton") { CheckResult result = check(R"( local foo: "hello" = "hello" local almostFoo: true = true - almostFoo = foo )"); - LUAU_REQUIRE_ERRORS(result); + LUAU_REQUIRE_NO_ERRORS(result); - TypeId foo = requireType("foo"); - TypeId almostFoo = requireType("almostFoo"); - std::string diffMessage; - try - { - diffMessage = diff(foo, almostFoo).diffError->toString(); - } - catch (const InternalCompilerError& e) - { - INFO(("InternalCompilerError: " + e.message)); - CHECK(false); - } - CHECK_EQ( - R"(DiffError: these two types are not equal because the left type at has type "hello", while the right type at has type true)", - diffMessage); + compareTypesNe("foo", "almostFoo", + R"(DiffError: these two types are not equal because the left type at has type "hello", while the right type at has type true)"); } -TEST_CASE_FIXTURE(Fixture, "equal_singleton") +TEST_CASE_FIXTURE(DifferFixture, "equal_singleton") { CheckResult result = check(R"( local foo: "hello" = "hello" local almostFoo: "hello" - almostFoo = foo )"); LUAU_REQUIRE_NO_ERRORS(result); - TypeId foo = requireType("foo"); - TypeId almostFoo = requireType("almostFoo"); - try - { - DifferResult diffRes = diff(foo, almostFoo); - INFO(diffRes.diffError->toString()); - CHECK(!diffRes.diffError.has_value()); - } - catch (const InternalCompilerError& e) - { - INFO(("InternalCompilerError: " + e.message)); - CHECK(false); - } + compareTypesEq("foo", "almostFoo"); } -TEST_CASE_FIXTURE(Fixture, "singleton_string") +TEST_CASE_FIXTURE(DifferFixture, "singleton_string") { CheckResult result = check(R"( local foo: "hello" = "hello" local almostFoo: "world" = "world" - almostFoo = foo )"); - LUAU_REQUIRE_ERRORS(result); + LUAU_REQUIRE_NO_ERRORS(result); - TypeId foo = requireType("foo"); - TypeId almostFoo = requireType("almostFoo"); - std::string diffMessage; - try - { - diffMessage = diff(foo, almostFoo).diffError->toString(); - } - catch (const InternalCompilerError& e) - { - INFO(("InternalCompilerError: " + e.message)); - CHECK(false); - } - CHECK_EQ( - R"(DiffError: these two types are not equal because the left type at has type "hello", while the right type at has type "world")", - diffMessage); + compareTypesNe("foo", "almostFoo", + R"(DiffError: these two types are not equal because the left type at has type "hello", while the right type at has type "world")"); } -TEST_CASE_FIXTURE(Fixture, "equal_function") +TEST_CASE_FIXTURE(DifferFixtureWithBuiltins, "negation") +{ + // Old solver does not correctly refine test types + ScopedFastFlag sff{"DebugLuauDeferredConstraintResolution", true}; + + CheckResult result = check(R"( + local bar: { x: { y: unknown }} + local almostBar: { x: { y: unknown }} + + local foo + local almostFoo + + if typeof(bar.x.y) ~= "string" then + foo = bar + end + + if typeof(almostBar.x.y) ~= "number" then + almostFoo = almostBar + end + + )"); + LUAU_REQUIRE_NO_ERRORS(result); + + compareTypesNe("foo", "almostFoo", + R"(DiffError: these two types are not equal because the left type at .x.y.Negation has type string, while the right type at .x.y.Negation has type number)"); +} + +TEST_CASE_FIXTURE(DifferFixture, "union_missing_right") +{ + CheckResult result = check(R"( + local foo: string | number + local almostFoo: boolean | string + )"); + LUAU_REQUIRE_NO_ERRORS(result); + + compareTypesNe("foo", "almostFoo", + R"(DiffError: these two types are not equal because the left type at is a union containing type number, while the right type at is a union missing type number)"); +} + +TEST_CASE_FIXTURE(DifferFixture, "union_missing_left") +{ + CheckResult result = check(R"( + local foo: string | number + local almostFoo: boolean | string | number + )"); + LUAU_REQUIRE_NO_ERRORS(result); + + compareTypesNe("foo", "almostFoo", + R"(DiffError: these two types are not equal because the left type at is a union missing type boolean, while the right type at is a union containing type boolean)"); +} + +TEST_CASE_FIXTURE(DifferFixture, "union_missing") +{ + // TODO: this test case produces an error message that is not the most UX-friendly + + CheckResult result = check(R"( + local foo: { bar: number, pan: string } | { baz: boolean, rot: "singleton" } + local almostFoo: { bar: number, pan: string } | { baz: string, rot: "singleton" } + )"); + LUAU_REQUIRE_NO_ERRORS(result); + + compareTypesNe("foo", "almostFoo", + R"(DiffError: these two types are not equal because the left type at is a union containing type {| baz: boolean, rot: "singleton" |}, while the right type at is a union missing type {| baz: boolean, rot: "singleton" |})"); +} + +TEST_CASE_FIXTURE(DifferFixture, "intersection_missing_right") +{ + CheckResult result = check(R"( + local foo: (number) -> () & (string) -> () + local almostFoo: (string) -> () & (boolean) -> () + )"); + LUAU_REQUIRE_NO_ERRORS(result); + + compareTypesNe("foo", "almostFoo", + R"(DiffError: these two types are not equal because the left type at is an intersection containing type (number) -> (), while the right type at is an intersection missing type (number) -> ())"); +} + +TEST_CASE_FIXTURE(DifferFixture, "intersection_missing_left") +{ + CheckResult result = check(R"( + local foo: (number) -> () & (string) -> () + local almostFoo: (string) -> () & (boolean) -> () & (number) -> () + )"); + LUAU_REQUIRE_NO_ERRORS(result); + + compareTypesNe("foo", "almostFoo", + R"(DiffError: these two types are not equal because the left type at is an intersection missing type (boolean) -> (), while the right type at is an intersection containing type (boolean) -> ())"); +} + +TEST_CASE_FIXTURE(DifferFixture, "intersection_tables_missing_right") +{ + CheckResult result = check(R"( + local foo: { x: number } & { y: string } + local almostFoo: { y: string } & { z: boolean } + )"); + LUAU_REQUIRE_NO_ERRORS(result); + + compareTypesNe("foo", "almostFoo", + R"(DiffError: these two types are not equal because the left type at is an intersection containing type {| x: number |}, while the right type at is an intersection missing type {| x: number |})"); +} + +TEST_CASE_FIXTURE(DifferFixture, "intersection_tables_missing_left") +{ + CheckResult result = check(R"( + local foo: { x: number } & { y: string } + local almostFoo: { y: string } & { z: boolean } & { x: number } + )"); + LUAU_REQUIRE_NO_ERRORS(result); + + compareTypesNe("foo", "almostFoo", + R"(DiffError: these two types are not equal because the left type at is an intersection missing type {| z: boolean |}, while the right type at is an intersection containing type {| z: boolean |})"); +} + +TEST_CASE_FIXTURE(DifferFixture, "equal_function") { // Old solver does not correctly infer function typepacks ScopedFastFlag sff{"DebugLuauDeferredConstraintResolution", true}; @@ -333,22 +291,10 @@ TEST_CASE_FIXTURE(Fixture, "equal_function") )"); LUAU_REQUIRE_NO_ERRORS(result); - TypeId foo = requireType("foo"); - TypeId almostFoo = requireType("almostFoo"); - try - { - DifferResult diffRes = diff(foo, almostFoo); - INFO(diffRes.diffError->toString()); - CHECK(!diffRes.diffError.has_value()); - } - catch (const InternalCompilerError& e) - { - INFO(("InternalCompilerError: " + e.message)); - CHECK(false); - } + compareTypesEq("foo", "almostFoo"); } -TEST_CASE_FIXTURE(Fixture, "equal_function_inferred_ret_length") +TEST_CASE_FIXTURE(DifferFixture, "equal_function_inferred_ret_length") { // Old solver does not correctly infer function typepacks ScopedFastFlag sff{"DebugLuauDeferredConstraintResolution", true}; @@ -369,22 +315,10 @@ TEST_CASE_FIXTURE(Fixture, "equal_function_inferred_ret_length") )"); LUAU_REQUIRE_NO_ERRORS(result); - TypeId foo = requireType("foo"); - TypeId almostFoo = requireType("almostFoo"); - try - { - DifferResult diffRes = diff(foo, almostFoo); - INFO(diffRes.diffError->toString()); - CHECK(!diffRes.diffError.has_value()); - } - catch (const InternalCompilerError& e) - { - INFO(("InternalCompilerError: " + e.message)); - CHECK(false); - } + compareTypesEq("foo", "almostFoo"); } -TEST_CASE_FIXTURE(Fixture, "equal_function_inferred_ret_length_2") +TEST_CASE_FIXTURE(DifferFixture, "equal_function_inferred_ret_length_2") { // Old solver does not correctly infer function typepacks ScopedFastFlag sff{"DebugLuauDeferredConstraintResolution", true}; @@ -402,22 +336,10 @@ TEST_CASE_FIXTURE(Fixture, "equal_function_inferred_ret_length_2") )"); LUAU_REQUIRE_NO_ERRORS(result); - TypeId foo = requireType("foo"); - TypeId almostFoo = requireType("almostFoo"); - try - { - DifferResult diffRes = diff(foo, almostFoo); - INFO(diffRes.diffError->toString()); - CHECK(!diffRes.diffError.has_value()); - } - catch (const InternalCompilerError& e) - { - INFO(("InternalCompilerError: " + e.message)); - CHECK(false); - } + compareTypesEq("foo", "almostFoo"); } -TEST_CASE_FIXTURE(Fixture, "function_arg_normal") +TEST_CASE_FIXTURE(DifferFixture, "function_arg_normal") { // Old solver does not correctly infer function typepacks ScopedFastFlag sff{"DebugLuauDeferredConstraintResolution", true}; @@ -428,28 +350,15 @@ TEST_CASE_FIXTURE(Fixture, "function_arg_normal") end function almostFoo(a: number, b: number, msg: string) return a - almostFoo = foo + end )"); - LUAU_REQUIRE_ERRORS(result); + LUAU_REQUIRE_NO_ERRORS(result); - TypeId foo = requireType("foo"); - TypeId almostFoo = requireType("almostFoo"); - std::string diffMessage; - try - { - diffMessage = diff(foo, almostFoo).diffError->toString(); - } - catch (const InternalCompilerError& e) - { - INFO(("InternalCompilerError: " + e.message)); - CHECK(false); - } - CHECK_EQ( - R"(DiffError: these two types are not equal because the left type at .Arg[3] has type number, while the right type at .Arg[3] has type string)", - diffMessage); + compareTypesNe("foo", "almostFoo", + R"(DiffError: these two types are not equal because the left type at .Arg[3] has type number, while the right type at .Arg[3] has type string)"); } -TEST_CASE_FIXTURE(Fixture, "function_arg_normal_2") +TEST_CASE_FIXTURE(DifferFixture, "function_arg_normal_2") { // Old solver does not correctly infer function typepacks ScopedFastFlag sff{"DebugLuauDeferredConstraintResolution", true}; @@ -460,28 +369,15 @@ TEST_CASE_FIXTURE(Fixture, "function_arg_normal_2") end function almostFoo(a: number, y: string, msg: string) return a - almostFoo = foo + end )"); - LUAU_REQUIRE_ERRORS(result); + LUAU_REQUIRE_NO_ERRORS(result); - TypeId foo = requireType("foo"); - TypeId almostFoo = requireType("almostFoo"); - std::string diffMessage; - try - { - diffMessage = diff(foo, almostFoo).diffError->toString(); - } - catch (const InternalCompilerError& e) - { - INFO(("InternalCompilerError: " + e.message)); - CHECK(false); - } - CHECK_EQ( - R"(DiffError: these two types are not equal because the left type at .Arg[2] has type number, while the right type at .Arg[2] has type string)", - diffMessage); + compareTypesNe("foo", "almostFoo", + R"(DiffError: these two types are not equal because the left type at .Arg[2] has type number, while the right type at .Arg[2] has type string)"); } -TEST_CASE_FIXTURE(Fixture, "function_ret_normal") +TEST_CASE_FIXTURE(DifferFixture, "function_ret_normal") { // Old solver does not correctly infer function typepacks ScopedFastFlag sff{"DebugLuauDeferredConstraintResolution", true}; @@ -494,31 +390,13 @@ TEST_CASE_FIXTURE(Fixture, "function_ret_normal") return msg end )"); + LUAU_REQUIRE_NO_ERRORS(result); - TypeId foo = requireType("foo"); - TypeId almostFoo = requireType("almostFoo"); - std::string diffMessage; - try - { - DifferResult diffRes = diff(foo, almostFoo); - if (!diffRes.diffError.has_value()) - { - INFO("Differ did not report type error, even though types are unequal"); - CHECK(false); - } - diffMessage = diffRes.diffError->toString(); - } - catch (const InternalCompilerError& e) - { - INFO(("InternalCompilerError: " + e.message)); - CHECK(false); - } - CHECK_EQ( - R"(DiffError: these two types are not equal because the left type at .Ret[1] has type number, while the right type at .Ret[1] has type string)", - diffMessage); + compareTypesNe("foo", "almostFoo", + R"(DiffError: these two types are not equal because the left type at .Ret[1] has type number, while the right type at .Ret[1] has type string)"); } -TEST_CASE_FIXTURE(Fixture, "function_arg_length") +TEST_CASE_FIXTURE(DifferFixture, "function_arg_length") { // Old solver does not correctly infer function typepacks ScopedFastFlag sff{"DebugLuauDeferredConstraintResolution", true}; @@ -531,31 +409,13 @@ TEST_CASE_FIXTURE(Fixture, "function_arg_length") return x end )"); + LUAU_REQUIRE_NO_ERRORS(result); - TypeId foo = requireType("foo"); - TypeId almostFoo = requireType("almostFoo"); - std::string diffMessage; - try - { - DifferResult diffRes = diff(foo, almostFoo); - if (!diffRes.diffError.has_value()) - { - INFO("Differ did not report type error, even though types are unequal"); - CHECK(false); - } - diffMessage = diffRes.diffError->toString(); - } - catch (const InternalCompilerError& e) - { - INFO(("InternalCompilerError: " + e.message)); - CHECK(false); - } - CHECK_EQ( - R"(DiffError: these two types are not equal because the left type at takes 2 or more arguments, while the right type at takes 3 or more arguments)", - diffMessage); + compareTypesNe("foo", "almostFoo", + R"(DiffError: these two types are not equal because the left type at takes 2 or more arguments, while the right type at takes 3 or more arguments)"); } -TEST_CASE_FIXTURE(Fixture, "function_arg_length_2") +TEST_CASE_FIXTURE(DifferFixture, "function_arg_length_2") { // Old solver does not correctly infer function typepacks ScopedFastFlag sff{"DebugLuauDeferredConstraintResolution", true}; @@ -568,31 +428,13 @@ TEST_CASE_FIXTURE(Fixture, "function_arg_length_2") return x end )"); + LUAU_REQUIRE_NO_ERRORS(result); - TypeId foo = requireType("foo"); - TypeId almostFoo = requireType("almostFoo"); - std::string diffMessage; - try - { - DifferResult diffRes = diff(foo, almostFoo); - if (!diffRes.diffError.has_value()) - { - INFO("Differ did not report type error, even though types are unequal"); - CHECK(false); - } - diffMessage = diffRes.diffError->toString(); - } - catch (const InternalCompilerError& e) - { - INFO(("InternalCompilerError: " + e.message)); - CHECK(false); - } - CHECK_EQ( - R"(DiffError: these two types are not equal because the left type at takes 3 or more arguments, while the right type at takes 2 or more arguments)", - diffMessage); + compareTypesNe("foo", "almostFoo", + R"(DiffError: these two types are not equal because the left type at takes 3 or more arguments, while the right type at takes 2 or more arguments)"); } -TEST_CASE_FIXTURE(Fixture, "function_arg_length_none") +TEST_CASE_FIXTURE(DifferFixture, "function_arg_length_none") { // Old solver does not correctly infer function typepacks ScopedFastFlag sff{"DebugLuauDeferredConstraintResolution", true}; @@ -605,31 +447,13 @@ TEST_CASE_FIXTURE(Fixture, "function_arg_length_none") return x end )"); + LUAU_REQUIRE_NO_ERRORS(result); - TypeId foo = requireType("foo"); - TypeId almostFoo = requireType("almostFoo"); - std::string diffMessage; - try - { - DifferResult diffRes = diff(foo, almostFoo); - if (!diffRes.diffError.has_value()) - { - INFO("Differ did not report type error, even though types are unequal"); - CHECK(false); - } - diffMessage = diffRes.diffError->toString(); - } - catch (const InternalCompilerError& e) - { - INFO(("InternalCompilerError: " + e.message)); - CHECK(false); - } - CHECK_EQ( - R"(DiffError: these two types are not equal because the left type at takes 0 or more arguments, while the right type at takes 2 or more arguments)", - diffMessage); + compareTypesNe("foo", "almostFoo", + R"(DiffError: these two types are not equal because the left type at takes 0 or more arguments, while the right type at takes 2 or more arguments)"); } -TEST_CASE_FIXTURE(Fixture, "function_arg_length_none_2") +TEST_CASE_FIXTURE(DifferFixture, "function_arg_length_none_2") { // Old solver does not correctly infer function typepacks ScopedFastFlag sff{"DebugLuauDeferredConstraintResolution", true}; @@ -642,31 +466,13 @@ TEST_CASE_FIXTURE(Fixture, "function_arg_length_none_2") return 5 end )"); + LUAU_REQUIRE_NO_ERRORS(result); - TypeId foo = requireType("foo"); - TypeId almostFoo = requireType("almostFoo"); - std::string diffMessage; - try - { - DifferResult diffRes = diff(foo, almostFoo); - if (!diffRes.diffError.has_value()) - { - INFO("Differ did not report type error, even though types are unequal"); - CHECK(false); - } - diffMessage = diffRes.diffError->toString(); - } - catch (const InternalCompilerError& e) - { - INFO(("InternalCompilerError: " + e.message)); - CHECK(false); - } - CHECK_EQ( - R"(DiffError: these two types are not equal because the left type at takes 1 or more arguments, while the right type at takes 0 or more arguments)", - diffMessage); + compareTypesNe("foo", "almostFoo", + R"(DiffError: these two types are not equal because the left type at takes 1 or more arguments, while the right type at takes 0 or more arguments)"); } -TEST_CASE_FIXTURE(Fixture, "function_ret_length") +TEST_CASE_FIXTURE(DifferFixture, "function_ret_length") { // Old solver does not correctly infer function typepacks ScopedFastFlag sff{"DebugLuauDeferredConstraintResolution", true}; @@ -679,31 +485,13 @@ TEST_CASE_FIXTURE(Fixture, "function_ret_length") return x, y end )"); + LUAU_REQUIRE_NO_ERRORS(result); - TypeId foo = requireType("foo"); - TypeId almostFoo = requireType("almostFoo"); - std::string diffMessage; - try - { - DifferResult diffRes = diff(foo, almostFoo); - if (!diffRes.diffError.has_value()) - { - INFO("Differ did not report type error, even though types are unequal"); - CHECK(false); - } - diffMessage = diffRes.diffError->toString(); - } - catch (const InternalCompilerError& e) - { - INFO(("InternalCompilerError: " + e.message)); - CHECK(false); - } - CHECK_EQ( - R"(DiffError: these two types are not equal because the left type at returns 1 values, while the right type at returns 2 values)", - diffMessage); + compareTypesNe("foo", "almostFoo", + R"(DiffError: these two types are not equal because the left type at returns 1 values, while the right type at returns 2 values)"); } -TEST_CASE_FIXTURE(Fixture, "function_ret_length_2") +TEST_CASE_FIXTURE(DifferFixture, "function_ret_length_2") { // Old solver does not correctly infer function typepacks ScopedFastFlag sff{"DebugLuauDeferredConstraintResolution", true}; @@ -716,31 +504,13 @@ TEST_CASE_FIXTURE(Fixture, "function_ret_length_2") return y, x end )"); + LUAU_REQUIRE_NO_ERRORS(result); - TypeId foo = requireType("foo"); - TypeId almostFoo = requireType("almostFoo"); - std::string diffMessage; - try - { - DifferResult diffRes = diff(foo, almostFoo); - if (!diffRes.diffError.has_value()) - { - INFO("Differ did not report type error, even though types are unequal"); - CHECK(false); - } - diffMessage = diffRes.diffError->toString(); - } - catch (const InternalCompilerError& e) - { - INFO(("InternalCompilerError: " + e.message)); - CHECK(false); - } - CHECK_EQ( - R"(DiffError: these two types are not equal because the left type at returns 3 values, while the right type at returns 2 values)", - diffMessage); + compareTypesNe("foo", "almostFoo", + R"(DiffError: these two types are not equal because the left type at returns 3 values, while the right type at returns 2 values)"); } -TEST_CASE_FIXTURE(Fixture, "function_ret_length_none") +TEST_CASE_FIXTURE(DifferFixture, "function_ret_length_none") { // Old solver does not correctly infer function typepacks ScopedFastFlag sff{"DebugLuauDeferredConstraintResolution", true}; @@ -753,31 +523,13 @@ TEST_CASE_FIXTURE(Fixture, "function_ret_length_none") return x end )"); + LUAU_REQUIRE_NO_ERRORS(result); - TypeId foo = requireType("foo"); - TypeId almostFoo = requireType("almostFoo"); - std::string diffMessage; - try - { - DifferResult diffRes = diff(foo, almostFoo); - if (!diffRes.diffError.has_value()) - { - INFO("Differ did not report type error, even though types are unequal"); - CHECK(false); - } - diffMessage = diffRes.diffError->toString(); - } - catch (const InternalCompilerError& e) - { - INFO(("InternalCompilerError: " + e.message)); - CHECK(false); - } - CHECK_EQ( - R"(DiffError: these two types are not equal because the left type at returns 0 values, while the right type at returns 1 values)", - diffMessage); + compareTypesNe("foo", "almostFoo", + R"(DiffError: these two types are not equal because the left type at returns 0 values, while the right type at returns 1 values)"); } -TEST_CASE_FIXTURE(Fixture, "function_ret_length_none_2") +TEST_CASE_FIXTURE(DifferFixture, "function_ret_length_none_2") { // Old solver does not correctly infer function typepacks ScopedFastFlag sff{"DebugLuauDeferredConstraintResolution", true}; @@ -790,31 +542,13 @@ TEST_CASE_FIXTURE(Fixture, "function_ret_length_none_2") return end )"); + LUAU_REQUIRE_NO_ERRORS(result); - TypeId foo = requireType("foo"); - TypeId almostFoo = requireType("almostFoo"); - std::string diffMessage; - try - { - DifferResult diffRes = diff(foo, almostFoo); - if (!diffRes.diffError.has_value()) - { - INFO("Differ did not report type error, even though types are unequal"); - CHECK(false); - } - diffMessage = diffRes.diffError->toString(); - } - catch (const InternalCompilerError& e) - { - INFO(("InternalCompilerError: " + e.message)); - CHECK(false); - } - CHECK_EQ( - R"(DiffError: these two types are not equal because the left type at returns 1 values, while the right type at returns 0 values)", - diffMessage); + compareTypesNe("foo", "almostFoo", + R"(DiffError: these two types are not equal because the left type at returns 1 values, while the right type at returns 0 values)"); } -TEST_CASE_FIXTURE(Fixture, "function_variadic_arg_normal") +TEST_CASE_FIXTURE(DifferFixture, "function_variadic_arg_normal") { // Old solver does not correctly infer function typepacks ScopedFastFlag sff{"DebugLuauDeferredConstraintResolution", true}; @@ -827,31 +561,13 @@ TEST_CASE_FIXTURE(Fixture, "function_variadic_arg_normal") return a, b end )"); + LUAU_REQUIRE_NO_ERRORS(result); - TypeId foo = requireType("foo"); - TypeId almostFoo = requireType("almostFoo"); - std::string diffMessage; - try - { - DifferResult diffRes = diff(foo, almostFoo); - if (!diffRes.diffError.has_value()) - { - INFO("Differ did not report type error, even though types are unequal"); - CHECK(false); - } - diffMessage = diffRes.diffError->toString(); - } - catch (const InternalCompilerError& e) - { - INFO(("InternalCompilerError: " + e.message)); - CHECK(false); - } - CHECK_EQ( - R"(DiffError: these two types are not equal because the left type at .Arg[Variadic] has type number, while the right type at .Arg[Variadic] has type string)", - diffMessage); + compareTypesNe("foo", "almostFoo", + R"(DiffError: these two types are not equal because the left type at .Arg[Variadic] has type number, while the right type at .Arg[Variadic] has type string)"); } -TEST_CASE_FIXTURE(Fixture, "function_variadic_arg_missing") +TEST_CASE_FIXTURE(DifferFixture, "function_variadic_arg_missing") { // Old solver does not correctly infer function typepacks ScopedFastFlag sff{"DebugLuauDeferredConstraintResolution", true}; @@ -864,31 +580,13 @@ TEST_CASE_FIXTURE(Fixture, "function_variadic_arg_missing") return a, b end )"); + LUAU_REQUIRE_NO_ERRORS(result); - TypeId foo = requireType("foo"); - TypeId almostFoo = requireType("almostFoo"); - std::string diffMessage; - try - { - DifferResult diffRes = diff(foo, almostFoo); - if (!diffRes.diffError.has_value()) - { - INFO("Differ did not report type error, even though types are unequal"); - CHECK(false); - } - diffMessage = diffRes.diffError->toString(); - } - catch (const InternalCompilerError& e) - { - INFO(("InternalCompilerError: " + e.message)); - CHECK(false); - } - CHECK_EQ( - R"(DiffError: these two types are not equal because the left type at .Arg[Variadic] has type number, while the right type at .Arg[Variadic] has type any)", - diffMessage); + compareTypesNe("foo", "almostFoo", + R"(DiffError: these two types are not equal because the left type at .Arg[Variadic] has type number, while the right type at .Arg[Variadic] has type any)"); } -TEST_CASE_FIXTURE(Fixture, "function_variadic_arg_missing_2") +TEST_CASE_FIXTURE(DifferFixture, "function_variadic_arg_missing_2") { // Old solver does not correctly infer function typepacks ScopedFastFlag sff{"DebugLuauDeferredConstraintResolution", true}; @@ -901,31 +599,13 @@ TEST_CASE_FIXTURE(Fixture, "function_variadic_arg_missing_2") return a, b end )"); + LUAU_REQUIRE_NO_ERRORS(result); - TypeId foo = requireType("foo"); - TypeId almostFoo = requireType("almostFoo"); - std::string diffMessage; - try - { - DifferResult diffRes = diff(foo, almostFoo); - if (!diffRes.diffError.has_value()) - { - INFO("Differ did not report type error, even though types are unequal"); - CHECK(false); - } - diffMessage = diffRes.diffError->toString(); - } - catch (const InternalCompilerError& e) - { - INFO(("InternalCompilerError: " + e.message)); - CHECK(false); - } - CHECK_EQ( - R"(DiffError: these two types are not equal because the left type at .Arg[Variadic] has type any, while the right type at .Arg[Variadic] has type string)", - diffMessage); + compareTypesNe("foo", "almostFoo", + R"(DiffError: these two types are not equal because the left type at .Arg[Variadic] has type any, while the right type at .Arg[Variadic] has type string)"); } -TEST_CASE_FIXTURE(Fixture, "function_variadic_oversaturation") +TEST_CASE_FIXTURE(DifferFixture, "function_variadic_oversaturation") { // Old solver does not correctly infer function typepacks ScopedFastFlag sff{"DebugLuauDeferredConstraintResolution", true}; @@ -938,31 +618,13 @@ TEST_CASE_FIXTURE(Fixture, "function_variadic_oversaturation") -- must not be oversaturated local almostFoo: (number, string) -> (number, string) = foo )"); + LUAU_REQUIRE_NO_ERRORS(result); - TypeId foo = requireType("foo"); - TypeId almostFoo = requireType("almostFoo"); - std::string diffMessage; - try - { - DifferResult diffRes = diff(foo, almostFoo); - if (!diffRes.diffError.has_value()) - { - INFO("Differ did not report type error, even though types are unequal"); - CHECK(false); - } - diffMessage = diffRes.diffError->toString(); - } - catch (const InternalCompilerError& e) - { - INFO(("InternalCompilerError: " + e.message)); - CHECK(false); - } - CHECK_EQ( - R"(DiffError: these two types are not equal because the left type at takes 2 or more arguments, while the right type at takes 2 arguments)", - diffMessage); + compareTypesNe("foo", "almostFoo", + R"(DiffError: these two types are not equal because the left type at takes 2 or more arguments, while the right type at takes 2 arguments)"); } -TEST_CASE_FIXTURE(Fixture, "function_variadic_oversaturation_2") +TEST_CASE_FIXTURE(DifferFixture, "function_variadic_oversaturation_2") { // Old solver does not correctly infer function typepacks ScopedFastFlag sff{"DebugLuauDeferredConstraintResolution", true}; @@ -975,28 +637,67 @@ TEST_CASE_FIXTURE(Fixture, "function_variadic_oversaturation_2") return x, y end )"); + LUAU_REQUIRE_NO_ERRORS(result); - TypeId foo = requireType("foo"); - TypeId almostFoo = requireType("almostFoo"); - std::string diffMessage; - try - { - DifferResult diffRes = diff(foo, almostFoo); - if (!diffRes.diffError.has_value()) - { - INFO("Differ did not report type error, even though types are unequal"); - CHECK(false); - } - diffMessage = diffRes.diffError->toString(); - } - catch (const InternalCompilerError& e) - { - INFO(("InternalCompilerError: " + e.message)); - CHECK(false); - } - CHECK_EQ( - R"(DiffError: these two types are not equal because the left type at takes 2 arguments, while the right type at takes 2 or more arguments)", - diffMessage); + compareTypesNe("foo", "almostFoo", + R"(DiffError: these two types are not equal because the left type at takes 2 arguments, while the right type at takes 2 or more arguments)"); +} + +TEST_CASE_FIXTURE(DifferFixture, "generic") +{ + // Old solver does not correctly infer function typepacks + ScopedFastFlag sff{"DebugLuauDeferredConstraintResolution", true}; + + CheckResult result = check(R"( + function foo(x, y) + return x, y + end + function almostFoo(x, y) + return y, x + end + )"); + LUAU_REQUIRE_NO_ERRORS(result); + + compareTypesNe("foo", "almostFoo", + R"(DiffError: these two types are not equal because the left generic at .Ret[1] cannot be the same type parameter as the right generic at .Ret[1])"); +} + +TEST_CASE_FIXTURE(DifferFixture, "generic_one_vs_two") +{ + // Old solver does not correctly infer function typepacks + ScopedFastFlag sff{"DebugLuauDeferredConstraintResolution", true}; + + CheckResult result = check(R"( + function foo(x: X, y: X) + return + end + function almostFoo(x: T, y: U) + return + end + )"); + LUAU_REQUIRE_NO_ERRORS(result); + + compareTypesNe("foo", "almostFoo", + R"(DiffError: these two types are not equal because the left generic at .Arg[2] cannot be the same type parameter as the right generic at .Arg[2])"); +} + +TEST_CASE_FIXTURE(DifferFixture, "generic_three_or_three") +{ + // Old solver does not correctly infer function typepacks + ScopedFastFlag sff{"DebugLuauDeferredConstraintResolution", true}; + + CheckResult result = check(R"( + function foo(x: X, y: X, z: Y) + return + end + function almostFoo(x: T, y: U, z: U) + return + end + )"); + LUAU_REQUIRE_NO_ERRORS(result); + + compareTypesNe("foo", "almostFoo", + R"(DiffError: these two types are not equal because the left generic at .Arg[2] cannot be the same type parameter as the right generic at .Arg[2])"); } TEST_SUITE_END(); diff --git a/tests/Fixture.cpp b/tests/Fixture.cpp index c6fc475b..d4fa7178 100644 --- a/tests/Fixture.cpp +++ b/tests/Fixture.cpp @@ -176,7 +176,7 @@ AstStatBlock* Fixture::parse(const std::string& source, const ParseOptions& pars if (FFlag::DebugLuauDeferredConstraintResolution) { ModulePtr module = Luau::check(*sourceModule, {}, builtinTypes, NotNull{&ice}, NotNull{&moduleResolver}, NotNull{&fileResolver}, - frontend.globals.globalScope, /*prepareModuleScope*/ nullptr, frontend.options); + frontend.globals.globalScope, /*prepareModuleScope*/ nullptr, frontend.options, {}); Luau::lint(sourceModule->root, *sourceModule->names, frontend.globals.globalScope, module.get(), sourceModule->hotcomments, {}); } @@ -415,6 +415,17 @@ TypeId Fixture::requireTypeAlias(const std::string& name) return *ty; } +TypeId Fixture::requireExportedType(const ModuleName& moduleName, const std::string& name) +{ + ModulePtr module = frontend.moduleResolver.getModule(moduleName); + REQUIRE(module); + + auto it = module->exportedTypeBindings.find(name); + REQUIRE(it != module->exportedTypeBindings.end()); + + return it->second.type; +} + std::string Fixture::decorateWithTypes(const std::string& code) { fileResolver.source[mainModuleName] = code; diff --git a/tests/Fixture.h b/tests/Fixture.h index 8d48ab1d..a9c5d9b0 100644 --- a/tests/Fixture.h +++ b/tests/Fixture.h @@ -2,6 +2,8 @@ #pragma once #include "Luau/Config.h" +#include "Luau/Differ.h" +#include "Luau/Error.h" #include "Luau/FileResolver.h" #include "Luau/Frontend.h" #include "Luau/IostreamHelpers.h" @@ -15,6 +17,7 @@ #include "IostreamOptional.h" #include "ScopedFlags.h" +#include "doctest.h" #include #include #include @@ -92,6 +95,7 @@ struct Fixture std::optional lookupType(const std::string& name); std::optional lookupImportedType(const std::string& moduleAlias, const std::string& name); TypeId requireTypeAlias(const std::string& name); + TypeId requireExportedType(const ModuleName& moduleName, const std::string& name); ScopedFastFlag sff_DebugLuauFreezeArena; @@ -153,6 +157,51 @@ std::optional linearSearchForBinding(Scope* scope, const char* name); void registerHiddenTypes(Frontend* frontend); void createSomeClasses(Frontend* frontend); +template +struct DifferFixtureGeneric : BaseFixture +{ + void compareNe(TypeId left, TypeId right, const std::string& expectedMessage) + { + std::string diffMessage; + try + { + DifferResult diffRes = diff(left, right); + REQUIRE_MESSAGE(diffRes.diffError.has_value(), "Differ did not report type error, even though types are unequal"); + diffMessage = diffRes.diffError->toString(); + } + catch (const InternalCompilerError& e) + { + REQUIRE_MESSAGE(false, ("InternalCompilerError: " + e.message)); + } + CHECK_EQ(expectedMessage, diffMessage); + } + + void compareTypesNe(const std::string& leftSymbol, const std::string& rightSymbol, const std::string& expectedMessage) + { + compareNe(BaseFixture::requireType(leftSymbol), BaseFixture::requireType(rightSymbol), expectedMessage); + } + + void compareEq(TypeId left, TypeId right) + { + try + { + DifferResult diffRes = diff(left, right); + CHECK_MESSAGE(!diffRes.diffError.has_value(), diffRes.diffError->toString()); + } + catch (const InternalCompilerError& e) + { + REQUIRE_MESSAGE(false, ("InternalCompilerError: " + e.message)); + } + } + + void compareTypesEq(const std::string& leftSymbol, const std::string& rightSymbol) + { + compareEq(BaseFixture::requireType(leftSymbol), BaseFixture::requireType(rightSymbol)); + } +}; +using DifferFixture = DifferFixtureGeneric; +using DifferFixtureWithBuiltins = DifferFixtureGeneric; + } // namespace Luau #define LUAU_REQUIRE_ERRORS(result) \ diff --git a/tests/Frontend.test.cpp b/tests/Frontend.test.cpp index 8f6834a1..fda0a6f0 100644 --- a/tests/Frontend.test.cpp +++ b/tests/Frontend.test.cpp @@ -444,6 +444,53 @@ TEST_CASE_FIXTURE(FrontendFixture, "cycle_incremental_type_surface_longer") CHECK_EQ(toString(tyB), "any"); } +TEST_CASE_FIXTURE(FrontendFixture, "cycle_incremental_type_surface_exports") +{ + ScopedFastFlag luauFixCyclicModuleExports{"LuauFixCyclicModuleExports", true}; + + fileResolver.source["game/A"] = R"( +local b = require(game.B) +export type atype = { x: b.btype } +return {mod_a = 1} + )"; + + fileResolver.source["game/B"] = R"( +export type btype = { x: number } + +local function bf() + local a = require(game.A) + local bfl : a.atype = nil + return {bfl.x} +end +return {mod_b = 2} + )"; + + ToStringOptions opts; + opts.exhaustive = true; + + CheckResult resultA = frontend.check("game/A"); + LUAU_REQUIRE_ERRORS(resultA); + + CheckResult resultB = frontend.check("game/B"); + LUAU_REQUIRE_ERRORS(resultB); + + TypeId tyB = requireExportedType("game/B", "btype"); + CHECK_EQ(toString(tyB, opts), "{| x: number |}"); + + TypeId tyA = requireExportedType("game/A", "atype"); + CHECK_EQ(toString(tyA, opts), "{| x: any |}"); + + frontend.markDirty("game/B"); + resultB = frontend.check("game/B"); + LUAU_REQUIRE_ERRORS(resultB); + + tyB = requireExportedType("game/B", "btype"); + CHECK_EQ(toString(tyB, opts), "{| x: number |}"); + + tyA = requireExportedType("game/A", "atype"); + CHECK_EQ(toString(tyA, opts), "{| x: any |}"); +} + TEST_CASE_FIXTURE(FrontendFixture, "dont_reparse_clean_file_when_linting") { fileResolver.source["Modules/A"] = R"( diff --git a/tests/Linter.test.cpp b/tests/Linter.test.cpp index 54a1f44c..e906c224 100644 --- a/tests/Linter.test.cpp +++ b/tests/Linter.test.cpp @@ -1657,6 +1657,8 @@ _ = (math.random() < 0.5 and false) or 42 -- currently ignored TEST_CASE_FIXTURE(Fixture, "WrongComment") { + ScopedFastFlag sff("LuauLintNativeComment", true); + LintResult result = lint(R"( --!strict --!struct @@ -1666,17 +1668,19 @@ TEST_CASE_FIXTURE(Fixture, "WrongComment") --!nolint UnknownGlobal --! no more lint --!strict here +--!native on do end --!nolint )"); - REQUIRE(6 == result.warnings.size()); + REQUIRE(7 == result.warnings.size()); CHECK_EQ(result.warnings[0].text, "Unknown comment directive 'struct'; did you mean 'strict'?"); CHECK_EQ(result.warnings[1].text, "Unknown comment directive 'nolintGlobal'"); CHECK_EQ(result.warnings[2].text, "nolint directive refers to unknown lint rule 'Global'"); CHECK_EQ(result.warnings[3].text, "nolint directive refers to unknown lint rule 'KnownGlobal'; did you mean 'UnknownGlobal'?"); CHECK_EQ(result.warnings[4].text, "Comment directive with the type checking mode has extra symbols at the end of the line"); - CHECK_EQ(result.warnings[5].text, "Comment directive is ignored because it is placed after the first non-comment token"); + CHECK_EQ(result.warnings[5].text, "native directive has extra symbols at the end of the line"); + CHECK_EQ(result.warnings[6].text, "Comment directive is ignored because it is placed after the first non-comment token"); } TEST_CASE_FIXTURE(Fixture, "WrongCommentMuteSelf") diff --git a/tests/Simplify.test.cpp b/tests/Simplify.test.cpp index 1223152b..63c03ba8 100644 --- a/tests/Simplify.test.cpp +++ b/tests/Simplify.test.cpp @@ -114,6 +114,17 @@ struct SimplifyFixture : Fixture TEST_SUITE_BEGIN("Simplify"); +TEST_CASE_FIXTURE(SimplifyFixture, "overload_negation_refinement_is_never") +{ + TypeId f1 = mkFunction(stringTy, numberTy); + TypeId f2 = mkFunction(numberTy, stringTy); + TypeId intersection = arena->addType(IntersectionType{{f1, f2}}); + TypeId unionT = arena->addType(UnionType{{errorTy, functionTy}}); + TypeId negationT = mkNegation(unionT); + // The intersection of string -> number & number -> string, ~(error | function) + CHECK(neverTy == intersect(intersection, negationT)); +} + TEST_CASE_FIXTURE(SimplifyFixture, "unknown_and_other_tops_and_bottom_types") { CHECK(unknownTy == intersect(unknownTy, unknownTy)); diff --git a/tests/StringUtils.test.cpp b/tests/StringUtils.test.cpp index 786f965e..cf65856d 100644 --- a/tests/StringUtils.test.cpp +++ b/tests/StringUtils.test.cpp @@ -59,7 +59,7 @@ TEST_CASE("BenchmarkLevenshteinDistance") auto end = std::chrono::steady_clock::now(); auto time = std::chrono::duration_cast(end - start); - std::cout << "Running levenshtein distance " << count << " times took " << time.count() << "ms" << std::endl; + MESSAGE("Running levenshtein distance ", count, " times took ", time.count(), "ms"); } #endif diff --git a/tests/ToString.test.cpp b/tests/ToString.test.cpp index 39759c71..d4a25f80 100644 --- a/tests/ToString.test.cpp +++ b/tests/ToString.test.cpp @@ -11,6 +11,7 @@ using namespace Luau; LUAU_FASTFLAG(LuauRecursiveTypeParameterRestriction); +LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution); TEST_SUITE_BEGIN("ToString"); diff --git a/tests/TypeInfer.anyerror.test.cpp b/tests/TypeInfer.anyerror.test.cpp index 687bc766..5601b8af 100644 --- a/tests/TypeInfer.anyerror.test.cpp +++ b/tests/TypeInfer.anyerror.test.cpp @@ -13,6 +13,8 @@ using namespace Luau; +LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution); + TEST_SUITE_BEGIN("TypeInferAnyError"); TEST_CASE_FIXTURE(Fixture, "for_in_loop_iterator_returns_any") diff --git a/tests/TypeInfer.classes.test.cpp b/tests/TypeInfer.classes.test.cpp index 07471d44..a4df1be7 100644 --- a/tests/TypeInfer.classes.test.cpp +++ b/tests/TypeInfer.classes.test.cpp @@ -12,6 +12,8 @@ using namespace Luau; using std::nullopt; +LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution); + TEST_SUITE_BEGIN("TypeInferClasses"); TEST_CASE_FIXTURE(ClassFixture, "call_method_of_a_class") diff --git a/tests/TypeInfer.functions.test.cpp b/tests/TypeInfer.functions.test.cpp index 268980fe..b37bcf83 100644 --- a/tests/TypeInfer.functions.test.cpp +++ b/tests/TypeInfer.functions.test.cpp @@ -14,7 +14,8 @@ using namespace Luau; -LUAU_FASTFLAG(LuauInstantiateInSubtyping) +LUAU_FASTFLAG(LuauInstantiateInSubtyping); +LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution); TEST_SUITE_BEGIN("TypeInferFunctions"); @@ -2094,6 +2095,25 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "attempt_to_call_an_intersection_of_tables_wi LUAU_REQUIRE_NO_ERRORS(result); } +TEST_CASE_FIXTURE(Fixture, "generic_packs_are_not_variadic") +{ + ScopedFastFlag sff{"DebugLuauDeferredConstraintResolution", true}; + + CheckResult result = check(R"( + local function apply(f: (a, b...) -> c..., x: a) + return f(x) + end + + local function add(x: number, y: number) + return x + y + end + + apply(add, 5) + )"); + + LUAU_REQUIRE_ERROR_COUNT(1, result); +} + TEST_CASE_FIXTURE(BuiltinsFixture, "num_is_solved_before_num_or_str") { CheckResult result = check(R"( diff --git a/tests/TypeInfer.generics.test.cpp b/tests/TypeInfer.generics.test.cpp index 72323cf9..35df644b 100644 --- a/tests/TypeInfer.generics.test.cpp +++ b/tests/TypeInfer.generics.test.cpp @@ -9,7 +9,8 @@ #include "doctest.h" -LUAU_FASTFLAG(LuauInstantiateInSubtyping) +LUAU_FASTFLAG(LuauInstantiateInSubtyping); +LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution); using namespace Luau; diff --git a/tests/TypeInfer.intersectionTypes.test.cpp b/tests/TypeInfer.intersectionTypes.test.cpp index 45d127ab..954d9858 100644 --- a/tests/TypeInfer.intersectionTypes.test.cpp +++ b/tests/TypeInfer.intersectionTypes.test.cpp @@ -8,6 +8,7 @@ using namespace Luau; +LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution); TEST_SUITE_BEGIN("IntersectionTypes"); diff --git a/tests/TypeInfer.provisional.test.cpp b/tests/TypeInfer.provisional.test.cpp index a1f456a3..6f4f9328 100644 --- a/tests/TypeInfer.provisional.test.cpp +++ b/tests/TypeInfer.provisional.test.cpp @@ -9,6 +9,8 @@ using namespace Luau; +LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution); + TEST_SUITE_BEGIN("ProvisionalTests"); // These tests check for behavior that differs from the final behavior we'd @@ -502,6 +504,9 @@ TEST_CASE_FIXTURE(Fixture, "free_options_cannot_be_unified_together") Normalizer normalizer{&arena, builtinTypes, NotNull{&sharedState}}; Unifier u{NotNull{&normalizer}, NotNull{scope.get()}, Location{}, Variance::Covariant}; + if (FFlag::DebugLuauDeferredConstraintResolution) + u.enableNewSolver(); + u.tryUnify(option1, option2); CHECK(!u.failure); @@ -565,7 +570,7 @@ return wrapStrictTable(Constants, "Constants") std::optional result = first(m->returnType); REQUIRE(result); if (FFlag::DebugLuauDeferredConstraintResolution) - CHECK_EQ("(any & ~table)?", toString(*result)); + CHECK_EQ("(any & ~(*error-type* | table))?", toString(*result)); else CHECK_MESSAGE(get(*result), *result); } @@ -905,6 +910,9 @@ TEST_CASE_FIXTURE(Fixture, "free_options_can_be_unified_together") Normalizer normalizer{&arena, builtinTypes, NotNull{&sharedState}}; Unifier u{NotNull{&normalizer}, NotNull{scope.get()}, Location{}, Variance::Covariant}; + if (FFlag::DebugLuauDeferredConstraintResolution) + u.enableNewSolver(); + u.tryUnify(option1, option2); CHECK(!u.failure); diff --git a/tests/TypeInfer.refinements.test.cpp b/tests/TypeInfer.refinements.test.cpp index 0c888740..ca302a2f 100644 --- a/tests/TypeInfer.refinements.test.cpp +++ b/tests/TypeInfer.refinements.test.cpp @@ -1820,4 +1820,46 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "refinements_should_preserve_error_suppressio LUAU_REQUIRE_NO_ERRORS(result); } +TEST_CASE_FIXTURE(BuiltinsFixture, "many_refinements_on_val") +{ + CheckResult result = check(R"( + local function is_nan(val: any): boolean + return type(val) == "number" and val ~= val + end + + local function is_js_boolean(val: any): boolean + return not not val and val ~= 0 and val ~= "" and not is_nan(val) + end + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + + CHECK_EQ("(any) -> boolean", toString(requireType("is_nan"))); + CHECK_EQ("(any) -> boolean", toString(requireType("is_js_boolean"))); +} + +TEST_CASE_FIXTURE(BuiltinsFixture, "refine_unknown_to_table") +{ + ScopedFastFlag sff{"DebugLuauDeferredConstraintResolution", true}; + // this test is DCR-only as an instance of DCR fixing a bug in the old solver + + CheckResult result = check(R"( + local a : unknown = nil + + local idx, val + + if typeof(a) == "table" then + for i, v in a do + idx = i + val = v + end + end + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + + CHECK_EQ("unknown", toString(requireType("idx"))); + CHECK_EQ("unknown", toString(requireType("val"))); +} + TEST_SUITE_END(); diff --git a/tests/TypeInfer.singletons.test.cpp b/tests/TypeInfer.singletons.test.cpp index c61ff16e..84e9fc7c 100644 --- a/tests/TypeInfer.singletons.test.cpp +++ b/tests/TypeInfer.singletons.test.cpp @@ -7,6 +7,8 @@ using namespace Luau; +LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution); + TEST_SUITE_BEGIN("TypeSingletons"); TEST_CASE_FIXTURE(Fixture, "function_args_infer_singletons") diff --git a/tests/TypeInfer.tables.test.cpp b/tests/TypeInfer.tables.test.cpp index e3d712be..8d93561f 100644 --- a/tests/TypeInfer.tables.test.cpp +++ b/tests/TypeInfer.tables.test.cpp @@ -132,6 +132,24 @@ TEST_CASE_FIXTURE(Fixture, "cannot_change_type_of_table_prop") LUAU_REQUIRE_ERROR_COUNT(1, result); } +TEST_CASE_FIXTURE(Fixture, "report_sensible_error_when_adding_a_value_to_a_nonexistent_prop") +{ + CheckResult result = check(R"( + local t = {} + t.foo[1] = 'one' + )"); + + LUAU_REQUIRE_ERROR_COUNT(1, result); + + INFO(result.errors[0]); + + UnknownProperty* err = get(result.errors[0]); + REQUIRE(err); + + CHECK("t" == toString(err->table)); + CHECK("foo" == err->key); +} + TEST_CASE_FIXTURE(Fixture, "function_calls_can_produce_tables") { CheckResult result = check("function get_table() return {prop=999} end get_table().prop = 0"); @@ -439,8 +457,6 @@ TEST_CASE_FIXTURE(Fixture, "table_param_row_polymorphism_2") )"); LUAU_REQUIRE_NO_ERRORS(result); - for (const auto& e : result.errors) - std::cout << "Error: " << e << std::endl; TypeId qType = requireType("q"); const TableType* qTable = get(qType); @@ -3642,4 +3658,75 @@ end LUAU_REQUIRE_NO_ERRORS(result); } +TEST_CASE_FIXTURE(Fixture, "certain_properties_of_table_literal_arguments_can_be_covariant") +{ + CheckResult result = check(R"( + function f(a: {[string]: string | {any} | nil }) + return a + end + + local x = f({ + title = "Feature.VirtualEvents.EnableNotificationsModalTitle", + body = "Feature.VirtualEvents.EnableNotificationsModalBody", + notNow = "Feature.VirtualEvents.NotNowButton", + getNotified = "Feature.VirtualEvents.GetNotifiedButton", + }) + )"); + + LUAU_REQUIRE_NO_ERRORS(result); +} + +TEST_CASE_FIXTURE(Fixture, "subproperties_can_also_be_covariantly_tested") +{ + CheckResult result = check(R"( + type T = { + [string]: {[string]: (string | number)?} + } + + function f(t: T) + return t + end + + local x = f({ + subprop={x="hello"} + }) + + local y = f({ + subprop={x=41} + }) + + local z = f({ + subprop={} + }) + )"); + + LUAU_REQUIRE_NO_ERRORS(result); +} + +TEST_CASE_FIXTURE(Fixture, "cyclic_shifted_tables") +{ + CheckResult result = check(R"( + local function id(x: a): a + return x + end + + -- Remove name from cyclic table + local foo = id({}) + foo.foo = id({}) + foo.foo.foo = id({}) + foo.foo.foo.foo = id({}) + foo.foo.foo.foo.foo = foo + + local almostFoo = id({}) + almostFoo.foo = id({}) + almostFoo.foo.foo = id({}) + almostFoo.foo.foo.foo = id({}) + almostFoo.foo.foo.foo.foo = almostFoo + -- Shift + almostFoo = almostFoo.foo.foo + )"); + + LUAU_REQUIRE_NO_ERRORS(result); +} + TEST_SUITE_END(); diff --git a/tests/TypeInfer.test.cpp b/tests/TypeInfer.test.cpp index 7ecde7fe..36422f8d 100644 --- a/tests/TypeInfer.test.cpp +++ b/tests/TypeInfer.test.cpp @@ -224,7 +224,7 @@ TEST_CASE_FIXTURE(Fixture, "crazy_complexity") A:A():A():A():A():A():A():A():A():A():A():A() )"); - std::cout << "OK! Allocated " << typeChecker.types.size() << " types" << std::endl; + MESSAGE("OK! Allocated ", typeChecker.types.size(), " types"); } #endif @@ -1332,4 +1332,43 @@ TEST_CASE_FIXTURE(Fixture, "handle_self_referential_HasProp_constraints") )"); } +/* We had an issue where we were unifying two type packs + * + * free-2-0... and (string, free-4-0...) + * + * The correct thing to do here is to promote everything on the right side to + * level 2-0 before binding the left pack to the right. If we fail to do this, + * then the code fragment here fails to typecheck because the argument and + * return types of C are generalized before we ever get to checking the body of + * C. + */ +TEST_CASE_FIXTURE(Fixture, "promote_tail_type_packs") +{ + CheckResult result = check(R"( + --!strict + + local A: any = nil + + local C + local D = A( + A({}, { + __call = function(a): string + local E: string = C(a) + return E + end + }), + { + F = function(s: typeof(C)) + end + } + ) + + function C(b: any): string + return '' + end + )"); + + LUAU_REQUIRE_NO_ERRORS(result); +} + TEST_SUITE_END(); diff --git a/tests/TypeInfer.tryUnify.test.cpp b/tests/TypeInfer.tryUnify.test.cpp index e00d5ae4..5656e871 100644 --- a/tests/TypeInfer.tryUnify.test.cpp +++ b/tests/TypeInfer.tryUnify.test.cpp @@ -345,7 +345,6 @@ TEST_CASE_FIXTURE(TryUnifyFixture, "metatables_unify_against_shape_of_free_table { ScopedFastFlag sff[] = { {"LuauTransitiveSubtyping", true}, - {"DebugLuauDeferredConstraintResolution", true}, }; TableType::Props freeProps{ @@ -369,6 +368,7 @@ TEST_CASE_FIXTURE(TryUnifyFixture, "metatables_unify_against_shape_of_free_table TypeId target = arena.addType(TableType{TableState::Unsealed, TypeLevel{}}); TypeId metatable = arena.addType(MetatableType{target, mt}); + state.enableNewSolver(); state.tryUnify(metatable, free); state.log.commit(); @@ -439,11 +439,10 @@ TEST_CASE_FIXTURE(TryUnifyFixture, "unifying_two_unions_under_dcr_does_not_creat const TypeId innerType = arena.freshType(nestedScope.get()); ScopedFastFlag sffs[]{ - {"DebugLuauDeferredConstraintResolution", true}, {"LuauAlwaysCommitInferencesOfFunctionCalls", true}, }; - state.enableScopeTests(); + state.enableNewSolver(); SUBCASE("equal_scopes") { diff --git a/tests/TypeInfer.typePacks.cpp b/tests/TypeInfer.typePacks.cpp index afe0552c..d2ae166b 100644 --- a/tests/TypeInfer.typePacks.cpp +++ b/tests/TypeInfer.typePacks.cpp @@ -9,6 +9,8 @@ using namespace Luau; +LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution); + TEST_SUITE_BEGIN("TypePackTests"); TEST_CASE_FIXTURE(Fixture, "infer_multi_return") diff --git a/tests/TypeInfer.unionTypes.test.cpp b/tests/TypeInfer.unionTypes.test.cpp index 12868d8b..3ab7bebb 100644 --- a/tests/TypeInfer.unionTypes.test.cpp +++ b/tests/TypeInfer.unionTypes.test.cpp @@ -8,6 +8,8 @@ using namespace Luau; +LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution); + TEST_SUITE_BEGIN("UnionTypes"); TEST_CASE_FIXTURE(Fixture, "return_types_can_be_disjoint") diff --git a/tests/TypeInfer.unknownnever.test.cpp b/tests/TypeInfer.unknownnever.test.cpp index e78c3d06..03859446 100644 --- a/tests/TypeInfer.unknownnever.test.cpp +++ b/tests/TypeInfer.unknownnever.test.cpp @@ -6,6 +6,8 @@ using namespace Luau; +LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution); + TEST_SUITE_BEGIN("TypeInferUnknownNever"); TEST_CASE_FIXTURE(Fixture, "string_subtype_and_unknown_supertype") diff --git a/tests/conformance/basic.lua b/tests/conformance/basic.lua index f4a91fc3..07150ba8 100644 --- a/tests/conformance/basic.lua +++ b/tests/conformance/basic.lua @@ -944,6 +944,11 @@ end)(true) == 5050) assert(pcall(typeof) == false) assert(pcall(type) == false) +function nothing() end + +assert(pcall(function() return typeof(nothing()) end) == false) +assert(pcall(function() return type(nothing()) end) == false) + -- typeof == type in absence of custom userdata assert(concat(typeof(5), typeof(nil), typeof({}), typeof(newproxy())) == "number,nil,table,userdata") diff --git a/tests/conformance/math.lua b/tests/conformance/math.lua index 47342730..9e9ae384 100644 --- a/tests/conformance/math.lua +++ b/tests/conformance/math.lua @@ -235,6 +235,12 @@ assert(flag); assert(select(2, pcall(math.random, 1, 2, 3)):match("wrong number of arguments")) +-- argument count +function nothing() end + +assert(pcall(math.abs) == false) +assert(pcall(function() return math.abs(nothing()) end) == false) + -- min/max assert(math.min(1) == 1) assert(math.min(1, 2) == 1) @@ -249,6 +255,7 @@ assert(math.max(1, -1, 2) == 2) assert(math.noise(0.5) == 0) assert(math.noise(0.5, 0.5) == -0.25) assert(math.noise(0.5, 0.5, -0.5) == 0.125) +assert(math.noise(455.7204209769105, 340.80410508750134, 121.80087666537628) == 0.5010709762573242) local inf = math.huge * 2 local nan = 0 / 0 diff --git a/tests/conformance/strings.lua b/tests/conformance/strings.lua index 702b51f8..370641d9 100644 --- a/tests/conformance/strings.lua +++ b/tests/conformance/strings.lua @@ -107,6 +107,12 @@ assert(tostring(1234567890123) == '1234567890123') assert(#tostring('\0') == 1) assert(tostring(true) == "true") assert(tostring(false) == "false") + +function nothing() end + +assert(pcall(tostring) == false) +assert(pcall(function() return tostring(nothing()) end) == false) + print('+') x = '"ílo"\n\\' diff --git a/tests/main.cpp b/tests/main.cpp index 5395e7c6..cfa1f9db 100644 --- a/tests/main.cpp +++ b/tests/main.cpp @@ -201,7 +201,7 @@ static FValueResult parseFFlag(std::string_view view) auto [name, value] = parseFValueHelper(view); bool state = value ? *value == "true" : true; if (value && value != "true" && value != "false") - std::cerr << "Ignored '" << name << "' because '" << *value << "' is not a valid FFlag state." << std::endl; + fprintf(stderr, "Ignored '%s' because '%s' is not a valid flag state\n", name.c_str(), value->c_str()); return {name, state}; } @@ -264,9 +264,7 @@ int main(int argc, char** argv) if (skipFastFlag(flag->name)) continue; - if (flag->dynamic) - std::cout << 'D'; - std::cout << "FFlag" << flag->name << std::endl; + printf("%sFFlag%s\n", flag->dynamic ? "D" : "", flag->name); } return 0; @@ -286,7 +284,7 @@ int main(int argc, char** argv) if (doctest::parseIntOption(argc, argv, "-O", doctest::option_int, level)) { if (level < 0 || level > 2) - std::cerr << "Optimization level must be between 0 and 2 inclusive." << std::endl; + fprintf(stderr, "Optimization level must be between 0 and 2 inclusive\n"); else optimizationLevel = level; } diff --git a/tools/faillist.txt b/tools/faillist.txt index 31c42eb8..3e2ee185 100644 --- a/tools/faillist.txt +++ b/tools/faillist.txt @@ -1,4 +1,6 @@ AstQuery.last_argument_function_call_type +AutocompleteTest.anonymous_autofilled_generic_on_argument_type_pack_vararg +AutocompleteTest.anonymous_autofilled_generic_type_pack_vararg BuiltinTests.aliased_string_format BuiltinTests.assert_removes_falsy_types BuiltinTests.assert_removes_falsy_types2 @@ -55,7 +57,6 @@ ProvisionalTests.typeguard_inference_incomplete RefinementTest.discriminate_from_truthiness_of_x RefinementTest.not_t_or_some_prop_of_t RefinementTest.refine_a_property_of_some_global -RefinementTest.refinements_should_preserve_error_suppression RefinementTest.truthy_constraint_on_properties RefinementTest.type_narrow_to_vector RefinementTest.typeguard_cast_free_table_to_vector @@ -69,9 +70,6 @@ TableTests.dont_crash_when_setmetatable_does_not_produce_a_metatabletypevar TableTests.dont_hang_when_trying_to_look_up_in_cyclic_metatable_index TableTests.dont_suggest_exact_match_keys TableTests.error_detailed_metatable_prop -TableTests.expected_indexer_from_table_union -TableTests.expected_indexer_value_type_extra -TableTests.expected_indexer_value_type_extra_2 TableTests.explicitly_typed_table TableTests.explicitly_typed_table_with_indexer TableTests.fuzz_table_unify_instantiated_table @@ -128,6 +126,7 @@ TypeInfer.follow_on_new_types_in_substitution TypeInfer.fuzz_free_table_type_change_during_index_check TypeInfer.infer_assignment_value_types_mutable_lval TypeInfer.no_stack_overflow_from_isoptional +TypeInfer.recursive_function_that_invokes_itself_with_a_refinement_of_its_parameter_2 TypeInfer.tc_after_error_recovery_no_replacement_name_in_error TypeInfer.type_infer_recursion_limit_no_ice TypeInfer.type_infer_recursion_limit_normalizer