From 685ca02a302e8613d7735cde5d225d1a979593e6 Mon Sep 17 00:00:00 2001 From: vegorov-rbx <75688451+vegorov-rbx@users.noreply.github.com> Date: Fri, 6 Jan 2023 12:17:25 -0800 Subject: [PATCH 1/4] Keep using ubuntu-20.04 in GitHub Actions (and clang++-10 in coverage) (#795) ### Problem ubuntu-latest was updated to 22.04 which removes clang++-10 we used for coverage stats and creates a pre-compiled binary that requires a glibc upgrade https://github.com/Roblox/luau/issues/773 ### Solution Pin to ubuntu-20.04 using multi-value matrix configurations. In coverage configuration, we use clang++-10 once again. --- .github/workflows/benchmark-dev.yml | 2 +- .github/workflows/build.yml | 12 ++++++------ .github/workflows/new-release.yml | 22 +++++++++++----------- .github/workflows/release.yml | 16 ++++++++-------- 4 files changed, 26 insertions(+), 26 deletions(-) diff --git a/.github/workflows/benchmark-dev.yml b/.github/workflows/benchmark-dev.yml index d78cb0ba..b6115acc 100644 --- a/.github/workflows/benchmark-dev.yml +++ b/.github/workflows/benchmark-dev.yml @@ -108,7 +108,7 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-latest, macos-latest] + os: [ubuntu-20.04, macos-latest] bench: - { script: "run-benchmarks", diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5145e76b..b084a4f5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -20,9 +20,9 @@ jobs: unix: strategy: matrix: - os: [ubuntu, macos] - name: ${{matrix.os}} - runs-on: ${{matrix.os}}-latest + os: [{name: ubuntu, version: ubuntu-20.04}, {name: macos, version: macos-latest}] + name: ${{matrix.os.name}} + runs-on: ${{matrix.os.version}} steps: - uses: actions/checkout@v1 - name: make tests @@ -81,7 +81,7 @@ jobs: Debug/luau-analyze tests/conformance/assert.lua coverage: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v2 - name: install @@ -89,7 +89,7 @@ jobs: sudo apt install llvm - name: make coverage run: | - CXX=clang++ make -j2 config=coverage native=1 coverage + CXX=clang++-10 make -j2 config=coverage native=1 coverage - name: upload coverage uses: codecov/codecov-action@v3 with: @@ -97,7 +97,7 @@ jobs: token: ${{ secrets.CODECOV_TOKEN }} web: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v1 - uses: actions/checkout@v2 diff --git a/.github/workflows/new-release.yml b/.github/workflows/new-release.yml index 90143323..5fe7f792 100644 --- a/.github/workflows/new-release.yml +++ b/.github/workflows/new-release.yml @@ -12,7 +12,7 @@ permissions: jobs: create-release: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 outputs: upload_url: ${{ steps.create_release.outputs.upload_url }} steps: @@ -30,9 +30,9 @@ jobs: needs: ["create-release"] strategy: matrix: - os: [ubuntu, macos, windows] - name: ${{matrix.os}} - runs-on: ${{matrix.os}}-latest + os: [{name: ubuntu, version: ubuntu-20.04}, {name: macos, version: macos-latest}, {name: windows, version: windows-latest}] + name: ${{matrix.os.name}} + runs-on: ${{matrix.os.version}} steps: - uses: actions/checkout@v1 - name: configure @@ -40,23 +40,23 @@ jobs: - name: build run: cmake --build . --target Luau.Repl.CLI Luau.Analyze.CLI --config Release -j 2 - name: pack - if: matrix.os != 'windows' - run: zip luau-${{matrix.os}}.zip luau* + if: matrix.os.name != 'windows' + run: zip luau-${{matrix.os.name}}.zip luau* - name: pack - if: matrix.os == 'windows' - run: 7z a luau-${{matrix.os}}.zip .\Release\luau*.exe + if: matrix.os.name == 'windows' + run: 7z a luau-${{matrix.os.name}}.zip .\Release\luau*.exe - uses: actions/upload-release-asset@v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: upload_url: ${{ needs.create-release.outputs.upload_url }} - asset_path: luau-${{matrix.os}}.zip - asset_name: luau-${{matrix.os}}.zip + asset_path: luau-${{matrix.os.name}}.zip + asset_name: luau-${{matrix.os.name}}.zip asset_content_type: application/octet-stream web: needs: ["create-release"] - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v1 - uses: actions/checkout@v2 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 36d3534c..497483a7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,9 +14,9 @@ jobs: build: strategy: matrix: - os: [ubuntu, macos, windows] - name: ${{matrix.os}} - runs-on: ${{matrix.os}}-latest + os: [{name: ubuntu, version: ubuntu-20.04}, {name: macos, version: macos-latest}, {name: windows, version: windows-latest}] + name: ${{matrix.os.name}} + runs-on: ${{matrix.os.version}} steps: - uses: actions/checkout@v1 - name: configure @@ -24,18 +24,18 @@ jobs: - name: build run: cmake --build . --target Luau.Repl.CLI Luau.Analyze.CLI --config Release -j 2 - uses: actions/upload-artifact@v2 - if: matrix.os != 'windows' + if: matrix.os.name != 'windows' with: - name: luau-${{matrix.os}} + name: luau-${{matrix.os.name}} path: luau* - uses: actions/upload-artifact@v2 - if: matrix.os == 'windows' + if: matrix.os.name == 'windows' with: - name: luau-${{matrix.os}} + name: luau-${{matrix.os.name}} path: Release\luau*.exe web: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v1 - uses: actions/checkout@v2 From be52bd91e44d15323dd44c58d7db861e58d9d764 Mon Sep 17 00:00:00 2001 From: vegorov-rbx <75688451+vegorov-rbx@users.noreply.github.com> Date: Fri, 6 Jan 2023 13:14:35 -0800 Subject: [PATCH 2/4] Sync to upstream/release/558 (#796) * Fixed garbage data in module scopes when type graph is not retained * LOP_MOVE with the same source and target registers is no longer generated (Fixes https://github.com/Roblox/luau/issues/793) --- Analysis/include/Luau/Constraint.h | 6 +- .../include/Luau/ConstraintGraphBuilder.h | 4 +- Analysis/include/Luau/Module.h | 15 +- Analysis/include/Luau/RecursionCounter.h | 2 +- Analysis/include/Luau/Type.h | 16 +- Analysis/include/Luau/TypeReduction.h | 40 + Analysis/src/AstQuery.cpp | 4 +- Analysis/src/Autocomplete.cpp | 36 +- Analysis/src/BuiltinDefinitions.cpp | 90 +- Analysis/src/ConstraintGraphBuilder.cpp | 21 +- Analysis/src/ConstraintSolver.cpp | 7 +- Analysis/src/EmbeddedBuiltinDefinitions.cpp | 8 +- Analysis/src/Frontend.cpp | 46 +- Analysis/src/Module.cpp | 32 +- Analysis/src/ToString.cpp | 7 +- Analysis/src/Type.cpp | 3 +- Analysis/src/TypeAttach.cpp | 7 +- Analysis/src/TypeChecker2.cpp | 103 +- Analysis/src/TypeInfer.cpp | 88 +- Analysis/src/TypeReduction.cpp | 838 +++++++++++ Compiler/src/Compiler.cpp | 5 +- Sources.cmake | 3 + tests/Autocomplete.test.cpp | 32 + tests/Compiler.test.cpp | 40 +- tests/ConstraintGraphBuilderFixture.cpp | 4 + tests/Fixture.cpp | 78 +- tests/Fixture.h | 4 +- tests/Frontend.test.cpp | 20 +- tests/Module.test.cpp | 24 +- tests/NonstrictMode.test.cpp | 4 +- tests/Normalize.test.cpp | 61 +- tests/TypeInfer.aliases.test.cpp | 19 +- tests/TypeInfer.annotations.test.cpp | 22 +- tests/TypeInfer.functions.test.cpp | 10 +- tests/TypeInfer.generics.test.cpp | 4 +- tests/TypeInfer.negations.test.cpp | 2 +- tests/TypeInfer.operators.test.cpp | 40 +- tests/TypeInfer.provisional.test.cpp | 16 +- tests/TypeInfer.tables.test.cpp | 26 +- tests/TypeInfer.test.cpp | 4 +- tests/TypeInfer.typePacks.cpp | 16 +- tests/TypeInfer.unionTypes.test.cpp | 2 - tests/TypeReduction.test.cpp | 1249 +++++++++++++++++ tests/TypeVar.test.cpp | 3 +- tools/faillist.txt | 4 +- 45 files changed, 2683 insertions(+), 382 deletions(-) create mode 100644 Analysis/include/Luau/TypeReduction.h create mode 100644 Analysis/src/TypeReduction.cpp create mode 100644 tests/TypeReduction.test.cpp diff --git a/Analysis/include/Luau/Constraint.h b/Analysis/include/Luau/Constraint.h index b4132954..ec94eee9 100644 --- a/Analysis/include/Luau/Constraint.h +++ b/Analysis/include/Luau/Constraint.h @@ -71,9 +71,9 @@ struct BinaryConstraint // When we dispatch this constraint, we update the key at this map to record // the overload that we selected. - AstExpr* expr; - DenseHashMap* astOriginalCallTypes; - DenseHashMap* astOverloadResolvedTypes; + const void* astFragment; + DenseHashMap* astOriginalCallTypes; + DenseHashMap* astOverloadResolvedTypes; }; // iteratee is iterable diff --git a/Analysis/include/Luau/ConstraintGraphBuilder.h b/Analysis/include/Luau/ConstraintGraphBuilder.h index 65ea5e09..3a67610a 100644 --- a/Analysis/include/Luau/ConstraintGraphBuilder.h +++ b/Analysis/include/Luau/ConstraintGraphBuilder.h @@ -82,11 +82,11 @@ struct ConstraintGraphBuilder // If the node was applied as a function, this is the unspecialized type of // that expression. - DenseHashMap astOriginalCallTypes{nullptr}; + DenseHashMap astOriginalCallTypes{nullptr}; // If overload resolution was performed on this element, this is the // overload that was selected. - DenseHashMap astOverloadResolvedTypes{nullptr}; + DenseHashMap astOverloadResolvedTypes{nullptr}; // Types resolved from type annotations. Analogous to astTypes. DenseHashMap astResolvedTypes{nullptr}; diff --git a/Analysis/include/Luau/Module.h b/Analysis/include/Luau/Module.h index d6d9f841..2cd6802e 100644 --- a/Analysis/include/Luau/Module.h +++ b/Analysis/include/Luau/Module.h @@ -73,19 +73,30 @@ struct Module DenseHashMap astTypes{nullptr}; DenseHashMap astTypePacks{nullptr}; DenseHashMap astExpectedTypes{nullptr}; - DenseHashMap astOriginalCallTypes{nullptr}; - DenseHashMap astOverloadResolvedTypes{nullptr}; + + // Pointers are either AstExpr or AstStat. + DenseHashMap astOriginalCallTypes{nullptr}; + + // Pointers are either AstExpr or AstStat. + DenseHashMap astOverloadResolvedTypes{nullptr}; + DenseHashMap astResolvedTypes{nullptr}; DenseHashMap astResolvedTypePacks{nullptr}; // Map AST nodes to the scope they create. Cannot be NotNull because we need a sentinel value for the map. DenseHashMap astScopes{nullptr}; + std::unique_ptr reduction; + std::unordered_map declaredGlobals; ErrorVec errors; Mode mode; SourceCode::Type type; bool timeout = false; + TypePackId returnType = nullptr; + std::unordered_map exportedTypeBindings; + + bool hasModuleScope() const; ScopePtr getModuleScope() const; // Once a module has been typechecked, we clone its public interface into a separate arena. diff --git a/Analysis/include/Luau/RecursionCounter.h b/Analysis/include/Luau/RecursionCounter.h index 77af10a0..0dc55700 100644 --- a/Analysis/include/Luau/RecursionCounter.h +++ b/Analysis/include/Luau/RecursionCounter.h @@ -32,7 +32,7 @@ struct RecursionCounter --(*count); } -private: +protected: int* count; }; diff --git a/Analysis/include/Luau/Type.h b/Analysis/include/Luau/Type.h index fcc073d8..734d40ea 100644 --- a/Analysis/include/Luau/Type.h +++ b/Analysis/include/Luau/Type.h @@ -494,13 +494,13 @@ struct AnyType { }; -// T | U +// `T | U` struct UnionType { std::vector options; }; -// T & U +// `T & U` struct IntersectionType { std::vector parts; @@ -519,9 +519,7 @@ struct NeverType { }; -// ~T -// TODO: Some simplification step that overwrites the type graph to make sure negation -// types disappear from the user's view, and (?) a debug flag to disable that +// `~T` struct NegationType { TypeId ty; @@ -676,6 +674,8 @@ TypeLevel* getMutableLevel(TypeId ty); std::optional getLevel(TypePackId tp); const Property* lookupClassProp(const ClassType* cls, const Name& name); + +// Whether `cls` is a subclass of `parent` bool isSubclass(const ClassType* cls, const ClassType* parent); Type* asMutable(TypeId ty); @@ -767,7 +767,7 @@ struct TypeIterator return !(*this == rhs); } - const TypeId& operator*() + TypeId operator*() { descend(); @@ -779,8 +779,8 @@ struct TypeIterator const std::vector& types = getTypes(t); LUAU_ASSERT(currentIndex < types.size()); - const TypeId& ty = types[currentIndex]; - LUAU_ASSERT(!get(follow(ty))); + TypeId ty = follow(types[currentIndex]); + LUAU_ASSERT(!get(ty)); return ty; } diff --git a/Analysis/include/Luau/TypeReduction.h b/Analysis/include/Luau/TypeReduction.h new file mode 100644 index 00000000..7df7edfa --- /dev/null +++ b/Analysis/include/Luau/TypeReduction.h @@ -0,0 +1,40 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#pragma once + +#include "Luau/Type.h" +#include "Luau/TypeArena.h" +#include "Luau/TypePack.h" +#include "Luau/Variant.h" + +namespace Luau +{ + +/// If it's desirable to allocate into a different arena than the TypeReduction instance you have, you will need +/// to create a temporary TypeReduction in that case. This is because TypeReduction caches the reduced type. +struct TypeReduction +{ + explicit TypeReduction(NotNull arena, NotNull builtinTypes, NotNull handle); + + std::optional reduce(TypeId ty); + std::optional reduce(TypePackId tp); + std::optional reduce(const TypeFun& fun); + +private: + NotNull arena; + NotNull builtinTypes; + NotNull handle; + + DenseHashMap cachedTypes{nullptr}; + DenseHashMap cachedTypePacks{nullptr}; + + std::optional reduceImpl(TypeId ty); + std::optional reduceImpl(TypePackId tp); + + // Computes an *estimated length* of the cartesian product of the given type. + size_t cartesianProductSize(TypeId ty) const; + + bool hasExceededCartesianProductLimit(TypeId ty) const; + bool hasExceededCartesianProductLimit(TypePackId tp) const; +}; + +} // namespace Luau diff --git a/Analysis/src/AstQuery.cpp b/Analysis/src/AstQuery.cpp index 39f613e5..ffab734a 100644 --- a/Analysis/src/AstQuery.cpp +++ b/Analysis/src/AstQuery.cpp @@ -256,7 +256,8 @@ AstExpr* findExprAtPosition(const SourceModule& source, Position pos) ScopePtr findScopeAtPosition(const Module& module, Position pos) { - LUAU_ASSERT(!module.scopes.empty()); + if (module.scopes.empty()) + return nullptr; Location scopeLocation = module.scopes.front().first; ScopePtr scope = module.scopes.front().second; @@ -320,7 +321,6 @@ std::optional findBindingAtPosition(const Module& module, const SourceM return std::nullopt; ScopePtr currentScope = findScopeAtPosition(module, pos); - LUAU_ASSERT(currentScope); while (currentScope) { diff --git a/Analysis/src/Autocomplete.cpp b/Analysis/src/Autocomplete.cpp index 7a649546..49c430e6 100644 --- a/Analysis/src/Autocomplete.cpp +++ b/Analysis/src/Autocomplete.cpp @@ -150,6 +150,8 @@ static TypeCorrectKind checkTypeCorrectKind( { ty = follow(ty); + LUAU_ASSERT(module.hasModuleScope()); + NotNull moduleScope{module.getModuleScope().get()}; auto typeAtPosition = findExpectedTypeAt(module, node, position); @@ -182,8 +184,7 @@ static TypeCorrectKind checkTypeCorrectKind( } } - return checkTypeMatch(ty, expectedType, NotNull{module.getModuleScope().get()}, typeArena, builtinTypes) ? TypeCorrectKind::Correct - : TypeCorrectKind::None; + return checkTypeMatch(ty, expectedType, moduleScope, typeArena, builtinTypes) ? TypeCorrectKind::Correct : TypeCorrectKind::None; } enum class PropIndexType @@ -1328,13 +1329,11 @@ static std::optional autocompleteStringParams(const Source } static AutocompleteResult autocomplete(const SourceModule& sourceModule, const ModulePtr& module, NotNull builtinTypes, - Scope* globalScope, Position position, StringCompletionCallback callback) + TypeArena* typeArena, Scope* globalScope, Position position, StringCompletionCallback callback) { if (isWithinComment(sourceModule, position)) return {}; - TypeArena typeArena; - std::vector ancestry = findAncestryAtPositionForAutocomplete(sourceModule, position); LUAU_ASSERT(!ancestry.empty()); AstNode* node = ancestry.back(); @@ -1360,7 +1359,7 @@ static AutocompleteResult autocomplete(const SourceModule& sourceModule, const M TypeId ty = follow(*it); PropIndexType indexType = indexName->op == ':' ? PropIndexType::Colon : PropIndexType::Point; - return {autocompleteProps(*module, &typeArena, builtinTypes, ty, indexType, ancestry), ancestry, AutocompleteContext::Property}; + return {autocompleteProps(*module, typeArena, builtinTypes, ty, indexType, ancestry), ancestry, AutocompleteContext::Property}; } else if (auto typeReference = node->as()) { @@ -1378,7 +1377,7 @@ static AutocompleteResult autocomplete(const SourceModule& sourceModule, const M if (statLocal->vars.size == 1 && (!statLocal->equalsSignLocation || position < statLocal->equalsSignLocation->begin)) return {{{"function", AutocompleteEntry{AutocompleteEntryKind::Keyword}}}, ancestry, AutocompleteContext::Unknown}; else if (statLocal->equalsSignLocation && position >= statLocal->equalsSignLocation->end) - return autocompleteExpression(sourceModule, *module, builtinTypes, &typeArena, ancestry, position); + return autocompleteExpression(sourceModule, *module, builtinTypes, typeArena, ancestry, position); else return {}; } @@ -1392,7 +1391,7 @@ static AutocompleteResult autocomplete(const SourceModule& sourceModule, const M if (statFor->from->location.containsClosed(position) || statFor->to->location.containsClosed(position) || (statFor->step && statFor->step->location.containsClosed(position))) - return autocompleteExpression(sourceModule, *module, builtinTypes, &typeArena, ancestry, position); + return autocompleteExpression(sourceModule, *module, builtinTypes, typeArena, ancestry, position); return {}; } @@ -1422,7 +1421,7 @@ static AutocompleteResult autocomplete(const SourceModule& sourceModule, const M AstExpr* lastExpr = statForIn->values.data[statForIn->values.size - 1]; if (lastExpr->location.containsClosed(position)) - return autocompleteExpression(sourceModule, *module, builtinTypes, &typeArena, ancestry, position); + return autocompleteExpression(sourceModule, *module, builtinTypes, typeArena, ancestry, position); if (position > lastExpr->location.end) return {{{"do", AutocompleteEntry{AutocompleteEntryKind::Keyword}}}, ancestry, AutocompleteContext::Keyword}; @@ -1446,7 +1445,7 @@ static AutocompleteResult autocomplete(const SourceModule& sourceModule, const M return {{{"do", AutocompleteEntry{AutocompleteEntryKind::Keyword}}}, ancestry, AutocompleteContext::Keyword}; if (!statWhile->hasDo || position < statWhile->doLocation.begin) - return autocompleteExpression(sourceModule, *module, builtinTypes, &typeArena, ancestry, position); + return autocompleteExpression(sourceModule, *module, builtinTypes, typeArena, ancestry, position); if (statWhile->hasDo && position > statWhile->doLocation.end) return {autocompleteStatement(sourceModule, *module, ancestry, position), ancestry, AutocompleteContext::Statement}; @@ -1463,7 +1462,7 @@ static AutocompleteResult autocomplete(const SourceModule& sourceModule, const M else if (AstStatIf* statIf = parent->as(); statIf && node->is()) { if (statIf->condition->is()) - return autocompleteExpression(sourceModule, *module, builtinTypes, &typeArena, ancestry, position); + return autocompleteExpression(sourceModule, *module, builtinTypes, typeArena, ancestry, position); else if (!statIf->thenLocation || statIf->thenLocation->containsClosed(position)) return {{{"then", AutocompleteEntry{AutocompleteEntryKind::Keyword}}}, ancestry, AutocompleteContext::Keyword}; } @@ -1471,7 +1470,7 @@ static AutocompleteResult autocomplete(const SourceModule& sourceModule, const M statIf && (!statIf->thenLocation || statIf->thenLocation->containsClosed(position))) return {{{"then", AutocompleteEntry{AutocompleteEntryKind::Keyword}}}, ancestry, AutocompleteContext::Keyword}; else if (AstStatRepeat* statRepeat = node->as(); statRepeat && statRepeat->condition->is()) - return autocompleteExpression(sourceModule, *module, builtinTypes, &typeArena, ancestry, position); + return autocompleteExpression(sourceModule, *module, builtinTypes, typeArena, ancestry, position); else if (AstStatRepeat* statRepeat = extractStat(ancestry); statRepeat) return {autocompleteStatement(sourceModule, *module, ancestry, position), ancestry, AutocompleteContext::Statement}; else if (AstExprTable* exprTable = parent->as(); @@ -1484,7 +1483,7 @@ static AutocompleteResult autocomplete(const SourceModule& sourceModule, const M { if (auto it = module->astExpectedTypes.find(exprTable)) { - auto result = autocompleteProps(*module, &typeArena, builtinTypes, *it, PropIndexType::Key, ancestry); + auto result = autocompleteProps(*module, typeArena, builtinTypes, *it, PropIndexType::Key, ancestry); if (FFlag::LuauCompleteTableKeysBetter) { @@ -1518,7 +1517,7 @@ static AutocompleteResult autocomplete(const SourceModule& sourceModule, const M // If we know for sure that a key is being written, do not offer general expression suggestions if (!key) - autocompleteExpression(sourceModule, *module, builtinTypes, &typeArena, ancestry, position, result); + autocompleteExpression(sourceModule, *module, builtinTypes, typeArena, ancestry, position, result); return {result, ancestry, AutocompleteContext::Property}; } @@ -1546,7 +1545,7 @@ static AutocompleteResult autocomplete(const SourceModule& sourceModule, const M if (auto idxExpr = ancestry.at(ancestry.size() - 2)->as()) { if (auto it = module->astTypes.find(idxExpr->expr)) - autocompleteProps(*module, &typeArena, builtinTypes, follow(*it), PropIndexType::Point, ancestry, result); + autocompleteProps(*module, typeArena, builtinTypes, follow(*it), PropIndexType::Point, ancestry, result); } else if (auto binExpr = ancestry.at(ancestry.size() - 2)->as()) { @@ -1572,7 +1571,7 @@ static AutocompleteResult autocomplete(const SourceModule& sourceModule, const M return {}; if (node->asExpr()) - return autocompleteExpression(sourceModule, *module, builtinTypes, &typeArena, ancestry, position); + return autocompleteExpression(sourceModule, *module, builtinTypes, typeArena, ancestry, position); else if (node->asStat()) return {autocompleteStatement(sourceModule, *module, ancestry, position), ancestry, AutocompleteContext::Statement}; @@ -1599,9 +1598,8 @@ AutocompleteResult autocomplete(Frontend& frontend, const ModuleName& moduleName NotNull builtinTypes = frontend.builtinTypes; Scope* globalScope = frontend.typeCheckerForAutocomplete.globalScope.get(); - AutocompleteResult autocompleteResult = autocomplete(*sourceModule, module, builtinTypes, globalScope, position, callback); - - return autocompleteResult; + TypeArena typeArena; + return autocomplete(*sourceModule, module, builtinTypes, &typeArena, globalScope, position, callback); } } // namespace Luau diff --git a/Analysis/src/BuiltinDefinitions.cpp b/Analysis/src/BuiltinDefinitions.cpp index 81702ff6..26aaf54f 100644 --- a/Analysis/src/BuiltinDefinitions.cpp +++ b/Analysis/src/BuiltinDefinitions.cpp @@ -18,9 +18,7 @@ LUAU_FASTFLAGVARIABLE(LuauSetMetaTableArgsCheck, false) LUAU_FASTFLAG(LuauUnknownAndNeverType) LUAU_FASTFLAGVARIABLE(LuauBuiltInMetatableNoBadSynthetic, false) -LUAU_FASTFLAG(LuauOptionalNextKey) LUAU_FASTFLAG(LuauReportShadowedTypeAlias) -LUAU_FASTFLAG(LuauNewLibraryTypeNames) /** FIXME: Many of these type definitions are not quite completely accurate. * @@ -289,38 +287,18 @@ void registerBuiltinGlobals(TypeChecker& typeChecker) addGlobalBinding(typeChecker, "string", it->second.type, "@luau"); - if (FFlag::LuauOptionalNextKey) - { - // next(t: Table, i: K?) -> (K?, V) - TypePackId nextArgsTypePack = arena.addTypePack(TypePack{{mapOfKtoV, makeOption(typeChecker, arena, genericK)}}); - TypePackId nextRetsTypePack = arena.addTypePack(TypePack{{makeOption(typeChecker, arena, genericK), genericV}}); - addGlobalBinding(typeChecker, "next", arena.addType(FunctionType{{genericK, genericV}, {}, nextArgsTypePack, nextRetsTypePack}), "@luau"); + // next(t: Table, i: K?) -> (K?, V) + TypePackId nextArgsTypePack = arena.addTypePack(TypePack{{mapOfKtoV, makeOption(typeChecker, arena, genericK)}}); + TypePackId nextRetsTypePack = arena.addTypePack(TypePack{{makeOption(typeChecker, arena, genericK), genericV}}); + addGlobalBinding(typeChecker, "next", arena.addType(FunctionType{{genericK, genericV}, {}, nextArgsTypePack, nextRetsTypePack}), "@luau"); - TypePackId pairsArgsTypePack = arena.addTypePack({mapOfKtoV}); + TypePackId pairsArgsTypePack = arena.addTypePack({mapOfKtoV}); - TypeId pairsNext = arena.addType(FunctionType{nextArgsTypePack, nextRetsTypePack}); - TypePackId pairsReturnTypePack = arena.addTypePack(TypePack{{pairsNext, mapOfKtoV, nilType}}); + TypeId pairsNext = arena.addType(FunctionType{nextArgsTypePack, nextRetsTypePack}); + TypePackId pairsReturnTypePack = arena.addTypePack(TypePack{{pairsNext, mapOfKtoV, nilType}}); - // pairs(t: Table) -> ((Table, K?) -> (K, V), Table, nil) - addGlobalBinding( - typeChecker, "pairs", arena.addType(FunctionType{{genericK, genericV}, {}, pairsArgsTypePack, pairsReturnTypePack}), "@luau"); - } - else - { - // next(t: Table, i: K?) -> (K, V) - TypePackId nextArgsTypePack = arena.addTypePack(TypePack{{mapOfKtoV, makeOption(typeChecker, arena, genericK)}}); - addGlobalBinding(typeChecker, "next", - arena.addType(FunctionType{{genericK, genericV}, {}, nextArgsTypePack, arena.addTypePack(TypePack{{genericK, genericV}})}), "@luau"); - - TypePackId pairsArgsTypePack = arena.addTypePack({mapOfKtoV}); - - TypeId pairsNext = arena.addType(FunctionType{nextArgsTypePack, arena.addTypePack(TypePack{{genericK, genericV}})}); - TypePackId pairsReturnTypePack = arena.addTypePack(TypePack{{pairsNext, mapOfKtoV, nilType}}); - - // pairs(t: Table) -> ((Table, K?) -> (K, V), Table, nil) - addGlobalBinding( - typeChecker, "pairs", arena.addType(FunctionType{{genericK, genericV}, {}, pairsArgsTypePack, pairsReturnTypePack}), "@luau"); - } + // pairs(t: Table) -> ((Table, K?) -> (K, V), Table, nil) + addGlobalBinding(typeChecker, "pairs", arena.addType(FunctionType{{genericK, genericV}, {}, pairsArgsTypePack, pairsReturnTypePack}), "@luau"); TypeId genericMT = arena.addType(GenericType{"MT"}); @@ -352,12 +330,7 @@ void registerBuiltinGlobals(TypeChecker& typeChecker) if (TableType* ttv = getMutable(pair.second.typeId)) { if (!ttv->name) - { - if (FFlag::LuauNewLibraryTypeNames) - ttv->name = "typeof(" + toString(pair.first) + ")"; - else - ttv->name = toString(pair.first); - } + ttv->name = "typeof(" + toString(pair.first) + ")"; } } @@ -408,36 +381,18 @@ void registerBuiltinGlobals(Frontend& frontend) addGlobalBinding(frontend, "string", it->second.type, "@luau"); - if (FFlag::LuauOptionalNextKey) - { - // next(t: Table, i: K?) -> (K?, V) - TypePackId nextArgsTypePack = arena.addTypePack(TypePack{{mapOfKtoV, makeOption(frontend, arena, genericK)}}); - TypePackId nextRetsTypePack = arena.addTypePack(TypePack{{makeOption(frontend, arena, genericK), genericV}}); - addGlobalBinding(frontend, "next", arena.addType(FunctionType{{genericK, genericV}, {}, nextArgsTypePack, nextRetsTypePack}), "@luau"); + // next(t: Table, i: K?) -> (K?, V) + TypePackId nextArgsTypePack = arena.addTypePack(TypePack{{mapOfKtoV, makeOption(frontend, arena, genericK)}}); + TypePackId nextRetsTypePack = arena.addTypePack(TypePack{{makeOption(frontend, arena, genericK), genericV}}); + addGlobalBinding(frontend, "next", arena.addType(FunctionType{{genericK, genericV}, {}, nextArgsTypePack, nextRetsTypePack}), "@luau"); - TypePackId pairsArgsTypePack = arena.addTypePack({mapOfKtoV}); + TypePackId pairsArgsTypePack = arena.addTypePack({mapOfKtoV}); - TypeId pairsNext = arena.addType(FunctionType{nextArgsTypePack, nextRetsTypePack}); - TypePackId pairsReturnTypePack = arena.addTypePack(TypePack{{pairsNext, mapOfKtoV, frontend.builtinTypes->nilType}}); + TypeId pairsNext = arena.addType(FunctionType{nextArgsTypePack, nextRetsTypePack}); + TypePackId pairsReturnTypePack = arena.addTypePack(TypePack{{pairsNext, mapOfKtoV, frontend.builtinTypes->nilType}}); - // pairs(t: Table) -> ((Table, K?) -> (K?, V), Table, nil) - addGlobalBinding(frontend, "pairs", arena.addType(FunctionType{{genericK, genericV}, {}, pairsArgsTypePack, pairsReturnTypePack}), "@luau"); - } - else - { - // next(t: Table, i: K?) -> (K, V) - TypePackId nextArgsTypePack = arena.addTypePack(TypePack{{mapOfKtoV, makeOption(frontend, arena, genericK)}}); - addGlobalBinding(frontend, "next", - arena.addType(FunctionType{{genericK, genericV}, {}, nextArgsTypePack, arena.addTypePack(TypePack{{genericK, genericV}})}), "@luau"); - - TypePackId pairsArgsTypePack = arena.addTypePack({mapOfKtoV}); - - TypeId pairsNext = arena.addType(FunctionType{nextArgsTypePack, arena.addTypePack(TypePack{{genericK, genericV}})}); - TypePackId pairsReturnTypePack = arena.addTypePack(TypePack{{pairsNext, mapOfKtoV, frontend.builtinTypes->nilType}}); - - // pairs(t: Table) -> ((Table, K?) -> (K, V), Table, nil) - addGlobalBinding(frontend, "pairs", arena.addType(FunctionType{{genericK, genericV}, {}, pairsArgsTypePack, pairsReturnTypePack}), "@luau"); - } + // pairs(t: Table) -> ((Table, K?) -> (K?, V), Table, nil) + addGlobalBinding(frontend, "pairs", arena.addType(FunctionType{{genericK, genericV}, {}, pairsArgsTypePack, pairsReturnTypePack}), "@luau"); TypeId genericMT = arena.addType(GenericType{"MT"}); @@ -469,12 +424,7 @@ void registerBuiltinGlobals(Frontend& frontend) if (TableType* ttv = getMutable(pair.second.typeId)) { if (!ttv->name) - { - if (FFlag::LuauNewLibraryTypeNames) - ttv->name = "typeof(" + toString(pair.first) + ")"; - else - ttv->name = toString(pair.first); - } + ttv->name = "typeof(" + toString(pair.first) + ")"; } } diff --git a/Analysis/src/ConstraintGraphBuilder.cpp b/Analysis/src/ConstraintGraphBuilder.cpp index 256eba54..6a80fed2 100644 --- a/Analysis/src/ConstraintGraphBuilder.cpp +++ b/Analysis/src/ConstraintGraphBuilder.cpp @@ -15,6 +15,7 @@ LUAU_FASTINT(LuauCheckRecursionLimit); LUAU_FASTFLAG(DebugLuauLogSolverToJson); LUAU_FASTFLAG(DebugLuauMagicTypes); LUAU_FASTFLAG(LuauNegatedClassTypes); +LUAU_FASTFLAG(LuauScopelessModule); namespace Luau { @@ -520,7 +521,8 @@ void ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatLocal* local) const Name name{local->vars.data[i]->name.value}; if (ModulePtr module = moduleResolver->getModule(moduleInfo->name)) - scope->importedTypeBindings[name] = module->getModuleScope()->exportedTypeBindings; + scope->importedTypeBindings[name] = + FFlag::LuauScopelessModule ? module->exportedTypeBindings : module->getModuleScope()->exportedTypeBindings; } } } @@ -733,16 +735,15 @@ void ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatAssign* assign) void ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatCompoundAssign* assign) { - // Synthesize A = A op B from A op= B and then build constraints for that instead. + // We need to tweak the BinaryConstraint that we emit, so we cannot use the + // strategy of falsifying an AST fragment. + TypeId varId = checkLValue(scope, assign->var); + Inference valueInf = check(scope, assign->value); - AstExprBinary exprBinary{assign->location, assign->op, assign->var, assign->value}; - AstExpr* exprBinaryPtr = &exprBinary; - - AstArray vars{&assign->var, 1}; - AstArray values{&exprBinaryPtr, 1}; - AstStatAssign syntheticAssign{assign->location, vars, values}; - - visit(scope, &syntheticAssign); + TypeId resultType = arena->addType(BlockedType{}); + addConstraint(scope, assign->location, + BinaryConstraint{assign->op, varId, valueInf.ty, resultType, assign, &astOriginalCallTypes, &astOverloadResolvedTypes}); + addConstraint(scope, assign->location, SubtypeConstraint{resultType, varId}); } void ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatIf* ifStatement) diff --git a/Analysis/src/ConstraintSolver.cpp b/Analysis/src/ConstraintSolver.cpp index 67c1732c..8092144c 100644 --- a/Analysis/src/ConstraintSolver.cpp +++ b/Analysis/src/ConstraintSolver.cpp @@ -18,6 +18,7 @@ LUAU_FASTFLAGVARIABLE(DebugLuauLogSolver, false); LUAU_FASTFLAGVARIABLE(DebugLuauLogSolverToJson, false); +LUAU_FASTFLAG(LuauScopelessModule); namespace Luau { @@ -681,8 +682,8 @@ bool ConstraintSolver::tryDispatch(const BinaryConstraint& c, NotNullty.emplace(mmResult); unblock(resultType); - (*c.astOriginalCallTypes)[c.expr] = *mm; - (*c.astOverloadResolvedTypes)[c.expr] = *instantiatedMm; + (*c.astOriginalCallTypes)[c.astFragment] = *mm; + (*c.astOverloadResolvedTypes)[c.astFragment] = *instantiatedMm; return true; } } @@ -1895,7 +1896,7 @@ TypeId ConstraintSolver::resolveModule(const ModuleInfo& info, const Location& l return errorRecoveryType(); } - TypePackId modulePack = module->getModuleScope()->returnType; + TypePackId modulePack = FFlag::LuauScopelessModule ? module->returnType : module->getModuleScope()->returnType; if (get(modulePack)) return errorRecoveryType(); diff --git a/Analysis/src/EmbeddedBuiltinDefinitions.cpp b/Analysis/src/EmbeddedBuiltinDefinitions.cpp index b0f21737..1fe09773 100644 --- a/Analysis/src/EmbeddedBuiltinDefinitions.cpp +++ b/Analysis/src/EmbeddedBuiltinDefinitions.cpp @@ -2,7 +2,6 @@ #include "Luau/BuiltinDefinitions.h" LUAU_FASTFLAG(LuauUnknownAndNeverType) -LUAU_FASTFLAG(LuauOptionalNextKey) namespace Luau { @@ -127,7 +126,7 @@ declare function rawlen(obj: {[K]: V} | string): number declare function setfenv(target: number | (T...) -> R..., env: {[string]: any}): ((T...) -> R...)? --- TODO: place ipairs definition here with removal of FFlagLuauOptionalNextKey +declare function ipairs(tab: {V}): (({V}, number) -> (number?, V), {V}, number) declare function pcall(f: (A...) -> R..., ...: A...): (boolean, R...) @@ -208,11 +207,6 @@ std::string getBuiltinDefinitionSource() else result += "declare function error(message: T, level: number?)\n"; - if (FFlag::LuauOptionalNextKey) - result += "declare function ipairs(tab: {V}): (({V}, number) -> (number?, V), {V}, number)\n"; - else - result += "declare function ipairs(tab: {V}): (({V}, number) -> (number, V), {V}, number)\n"; - return result; } diff --git a/Analysis/src/Frontend.cpp b/Analysis/src/Frontend.cpp index 5d2c1587..f4e529db 100644 --- a/Analysis/src/Frontend.cpp +++ b/Analysis/src/Frontend.cpp @@ -16,6 +16,7 @@ #include "Luau/TimeTrace.h" #include "Luau/TypeChecker2.h" #include "Luau/TypeInfer.h" +#include "Luau/TypeReduction.h" #include "Luau/Variant.h" #include @@ -30,6 +31,7 @@ LUAU_FASTFLAGVARIABLE(LuauKnowsTheDataModel3, false) LUAU_FASTINTVARIABLE(LuauAutocompleteCheckTimeoutMs, 100) LUAU_FASTFLAGVARIABLE(DebugLuauDeferredConstraintResolution, false) LUAU_FASTFLAG(DebugLuauLogSolverToJson); +LUAU_FASTFLAG(LuauScopelessModule); namespace Luau { @@ -111,7 +113,9 @@ LoadDefinitionFileResult Frontend::loadDefinitionFile(std::string_view source, c CloneState cloneState; std::vector typesToPersist; - typesToPersist.reserve(checkedModule->declaredGlobals.size() + checkedModule->getModuleScope()->exportedTypeBindings.size()); + typesToPersist.reserve( + checkedModule->declaredGlobals.size() + + (FFlag::LuauScopelessModule ? checkedModule->exportedTypeBindings.size() : checkedModule->getModuleScope()->exportedTypeBindings.size())); for (const auto& [name, ty] : checkedModule->declaredGlobals) { @@ -123,7 +127,8 @@ LoadDefinitionFileResult Frontend::loadDefinitionFile(std::string_view source, c typesToPersist.push_back(globalTy); } - for (const auto& [name, ty] : checkedModule->getModuleScope()->exportedTypeBindings) + for (const auto& [name, ty] : + FFlag::LuauScopelessModule ? checkedModule->exportedTypeBindings : checkedModule->getModuleScope()->exportedTypeBindings) { TypeFun globalTy = clone(ty, globalTypes, cloneState); std::string documentationSymbol = packageName + "/globaltype/" + name; @@ -168,7 +173,9 @@ LoadDefinitionFileResult loadDefinitionFile(TypeChecker& typeChecker, ScopePtr t CloneState cloneState; std::vector typesToPersist; - typesToPersist.reserve(checkedModule->declaredGlobals.size() + checkedModule->getModuleScope()->exportedTypeBindings.size()); + typesToPersist.reserve( + checkedModule->declaredGlobals.size() + + (FFlag::LuauScopelessModule ? checkedModule->exportedTypeBindings.size() : checkedModule->getModuleScope()->exportedTypeBindings.size())); for (const auto& [name, ty] : checkedModule->declaredGlobals) { @@ -180,7 +187,8 @@ LoadDefinitionFileResult loadDefinitionFile(TypeChecker& typeChecker, ScopePtr t typesToPersist.push_back(globalTy); } - for (const auto& [name, ty] : checkedModule->getModuleScope()->exportedTypeBindings) + for (const auto& [name, ty] : + FFlag::LuauScopelessModule ? checkedModule->exportedTypeBindings : checkedModule->getModuleScope()->exportedTypeBindings) { TypeFun globalTy = clone(ty, typeChecker.globalTypes, cloneState); std::string documentationSymbol = packageName + "/globaltype/" + name; @@ -562,12 +570,29 @@ CheckResult Frontend::check(const ModuleName& name, std::optionalinterfaceTypes); module->internalTypes.clear(); - module->astTypes.clear(); - module->astExpectedTypes.clear(); - module->astOriginalCallTypes.clear(); - module->astResolvedTypes.clear(); - module->astResolvedTypePacks.clear(); - module->scopes.resize(1); + + if (FFlag::LuauScopelessModule) + { + module->astTypes.clear(); + module->astTypePacks.clear(); + module->astExpectedTypes.clear(); + module->astOriginalCallTypes.clear(); + module->astOverloadResolvedTypes.clear(); + module->astResolvedTypes.clear(); + module->astResolvedTypePacks.clear(); + module->astScopes.clear(); + + module->scopes.clear(); + } + else + { + module->astTypes.clear(); + module->astExpectedTypes.clear(); + module->astOriginalCallTypes.clear(); + module->astResolvedTypes.clear(); + module->astResolvedTypePacks.clear(); + module->scopes.resize(1); + } } if (mode != Mode::NoCheck) @@ -852,6 +877,7 @@ ModulePtr Frontend::check( const SourceModule& sourceModule, Mode mode, const ScopePtr& environmentScope, std::vector requireCycles, bool forAutocomplete) { ModulePtr result = std::make_shared(); + result->reduction = std::make_unique(NotNull{&result->internalTypes}, builtinTypes, NotNull{&iceHandler}); std::unique_ptr logger; if (FFlag::DebugLuauLogSolverToJson) diff --git a/Analysis/src/Module.cpp b/Analysis/src/Module.cpp index a73b928b..e54a4493 100644 --- a/Analysis/src/Module.cpp +++ b/Analysis/src/Module.cpp @@ -7,9 +7,10 @@ #include "Luau/Normalize.h" #include "Luau/RecursionCounter.h" #include "Luau/Scope.h" +#include "Luau/Type.h" #include "Luau/TypeInfer.h" #include "Luau/TypePack.h" -#include "Luau/Type.h" +#include "Luau/TypeReduction.h" #include "Luau/VisitType.h" #include @@ -17,6 +18,7 @@ LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution); LUAU_FASTFLAGVARIABLE(LuauClonePublicInterfaceLess, false); LUAU_FASTFLAG(LuauSubstitutionReentrant); +LUAU_FASTFLAG(LuauScopelessModule); LUAU_FASTFLAG(LuauClassTypeVarsInSubstitution); LUAU_FASTFLAG(LuauSubstitutionFixMissingFields); @@ -189,7 +191,6 @@ void Module::clonePublicInterface(NotNull builtinTypes, InternalEr TypePackId returnType = moduleScope->returnType; std::optional varargPack = FFlag::DebugLuauDeferredConstraintResolution ? std::nullopt : moduleScope->varargPack; - std::unordered_map* exportedTypeBindings = &moduleScope->exportedTypeBindings; TxnLog log; ClonePublicInterface clonePublicInterface{&log, builtinTypes, this}; @@ -209,15 +210,12 @@ void Module::clonePublicInterface(NotNull builtinTypes, InternalEr moduleScope->varargPack = varargPack; } - if (exportedTypeBindings) + for (auto& [name, tf] : moduleScope->exportedTypeBindings) { - for (auto& [name, tf] : *exportedTypeBindings) - { - if (FFlag::LuauClonePublicInterfaceLess) - tf = clonePublicInterface.cloneTypeFun(tf); - else - tf = clone(tf, interfaceTypes, cloneState); - } + if (FFlag::LuauClonePublicInterfaceLess) + tf = clonePublicInterface.cloneTypeFun(tf); + else + tf = clone(tf, interfaceTypes, cloneState); } for (auto& [name, ty] : declaredGlobals) @@ -228,13 +226,25 @@ void Module::clonePublicInterface(NotNull builtinTypes, InternalEr ty = clone(ty, interfaceTypes, cloneState); } + // Copy external stuff over to Module itself + if (FFlag::LuauScopelessModule) + { + this->returnType = moduleScope->returnType; + this->exportedTypeBindings = std::move(moduleScope->exportedTypeBindings); + } + freeze(internalTypes); freeze(interfaceTypes); } +bool Module::hasModuleScope() const +{ + return !scopes.empty(); +} + ScopePtr Module::getModuleScope() const { - LUAU_ASSERT(!scopes.empty()); + LUAU_ASSERT(hasModuleScope()); return scopes.front().second; } diff --git a/Analysis/src/ToString.cpp b/Analysis/src/ToString.cpp index 05e51211..af038f18 100644 --- a/Analysis/src/ToString.cpp +++ b/Analysis/src/ToString.cpp @@ -26,6 +26,7 @@ LUAU_FASTFLAGVARIABLE(LuauSerializeNilUnionAsNil, false) * Fair warning: Setting this will break a lot of Luau unit tests. */ LUAU_FASTFLAGVARIABLE(DebugLuauVerboseTypeNames, false) +LUAU_FASTFLAGVARIABLE(DebugLuauToStringNoLexicalSort, false) namespace Luau { @@ -756,7 +757,8 @@ struct TypeStringifier state.unsee(&uv); - std::sort(results.begin(), results.end()); + if (!FFlag::DebugLuauToStringNoLexicalSort) + std::sort(results.begin(), results.end()); if (optional && results.size() > 1) state.emit("("); @@ -821,7 +823,8 @@ struct TypeStringifier state.unsee(&uv); - std::sort(results.begin(), results.end()); + if (!FFlag::DebugLuauToStringNoLexicalSort) + std::sort(results.begin(), results.end()); bool first = true; for (std::string& ss : results) diff --git a/Analysis/src/Type.cpp b/Analysis/src/Type.cpp index aba6bddc..f03061a8 100644 --- a/Analysis/src/Type.cpp +++ b/Analysis/src/Type.cpp @@ -26,7 +26,6 @@ LUAU_FASTINTVARIABLE(LuauTableTypeMaximumStringifierLength, 0) LUAU_FASTINT(LuauTypeInferRecursionLimit) LUAU_FASTFLAG(LuauUnknownAndNeverType) LUAU_FASTFLAGVARIABLE(LuauMaybeGenericIntersectionTypes, false) -LUAU_FASTFLAGVARIABLE(LuauNewLibraryTypeNames, false) LUAU_FASTFLAG(LuauInstantiateInSubtyping) namespace Luau @@ -865,7 +864,7 @@ TypeId BuiltinTypes::makeStringMetatable() TypeId tableType = arena->addType(TableType{std::move(stringLib), std::nullopt, TypeLevel{}, TableState::Sealed}); if (TableType* ttv = getMutable(tableType)) - ttv->name = FFlag::LuauNewLibraryTypeNames ? "typeof(string)" : "string"; + ttv->name = "typeof(string)"; return arena->addType(TableType{{{{"__index", {tableType}}}}, std::nullopt, TypeLevel{}, TableState::Sealed}); } diff --git a/Analysis/src/TypeAttach.cpp b/Analysis/src/TypeAttach.cpp index d1d89b25..f9a16205 100644 --- a/Analysis/src/TypeAttach.cpp +++ b/Analysis/src/TypeAttach.cpp @@ -487,8 +487,11 @@ public: AstType* annotation = local->annotation; if (!annotation) { - if (auto result = getScope(local->location)->lookup(local)) - local->annotation = typeAst(*result); + if (auto scope = getScope(local->location)) + { + if (auto result = scope->lookup(local)) + local->annotation = typeAst(*result); + } } return true; } diff --git a/Analysis/src/TypeChecker2.cpp b/Analysis/src/TypeChecker2.cpp index 5451a454..1d212851 100644 --- a/Analysis/src/TypeChecker2.cpp +++ b/Analysis/src/TypeChecker2.cpp @@ -626,8 +626,11 @@ struct TypeChecker2 void visit(AstStatCompoundAssign* stat) { - visit(stat->var); - visit(stat->value); + AstExprBinary fake{stat->location, stat->op, stat->var, stat->value}; + TypeId resultTy = visit(&fake, stat); + TypeId varTy = lookupType(stat->var); + + reportErrors(tryUnify(stack.back(), stat->location, resultTy, varTy)); } void visit(AstStatFunction* stat) @@ -737,7 +740,10 @@ struct TypeChecker2 else if (auto e = expr->as()) return visit(e); else if (auto e = expr->as()) - return visit(e); + { + visit(e); + return; + } else if (auto e = expr->as()) return visit(e); else if (auto e = expr->as()) @@ -1045,7 +1051,7 @@ struct TypeChecker2 } } - void visit(AstExprBinary* expr) + TypeId visit(AstExprBinary* expr, void* overrideKey = nullptr) { visit(expr->left); visit(expr->right); @@ -1066,8 +1072,10 @@ struct TypeChecker2 bool isStringOperation = isString(leftType) && isString(rightType); - if (get(leftType) || get(leftType) || get(rightType) || get(rightType)) - return; + if (get(leftType) || get(leftType)) + return leftType; + else if (get(rightType) || get(rightType)) + return rightType; if ((get(leftType) || get(leftType)) && !isEquality && !isLogical) { @@ -1075,14 +1083,13 @@ struct TypeChecker2 reportError(CannotInferBinaryOperation{expr->op, name, isComparison ? CannotInferBinaryOperation::OpKind::Comparison : CannotInferBinaryOperation::OpKind::Operation}, expr->location); - return; + return leftType; } if (auto it = kBinaryOpMetamethods.find(expr->op); it != kBinaryOpMetamethods.end()) { std::optional leftMt = getMetatable(leftType, builtinTypes); std::optional rightMt = getMetatable(rightType, builtinTypes); - bool matches = leftMt == rightMt; if (isEquality && !matches) { @@ -1114,7 +1121,7 @@ struct TypeChecker2 toString(leftType).c_str(), toString(rightType).c_str(), toString(expr->op).c_str())}, expr->location); - return; + return builtinTypes->errorRecoveryType(); } std::optional mm; @@ -1128,7 +1135,11 @@ struct TypeChecker2 if (mm) { - TypeId instantiatedMm = module->astOverloadResolvedTypes[expr]; + void* key = expr; + if (overrideKey != nullptr) + key = overrideKey; + + TypeId instantiatedMm = module->astOverloadResolvedTypes[key]; if (!instantiatedMm) reportError(CodeTooComplex{}, expr->location); @@ -1146,20 +1157,50 @@ struct TypeChecker2 expectedArgs = testArena.addTypePack({leftType, rightType}); } - reportErrors(tryUnify(scope, expr->location, ftv->argTypes, expectedArgs)); - + TypePackId expectedRets; if (expr->op == AstExprBinary::CompareEq || expr->op == AstExprBinary::CompareNe || expr->op == AstExprBinary::CompareGe || expr->op == AstExprBinary::CompareGt || expr->op == AstExprBinary::Op::CompareLe || expr->op == AstExprBinary::Op::CompareLt) { - TypePackId expectedRets = testArena.addTypePack({builtinTypes->booleanType}); - if (!isSubtype(ftv->retTypes, expectedRets, scope)) + expectedRets = testArena.addTypePack({builtinTypes->booleanType}); + } + else + { + expectedRets = testArena.addTypePack({testArena.freshType(scope, TypeLevel{})}); + } + + TypeId expectedTy = testArena.addType(FunctionType(expectedArgs, expectedRets)); + + reportErrors(tryUnify(scope, expr->location, follow(*mm), expectedTy)); + + std::optional ret = first(ftv->retTypes); + if (ret) + { + if (isComparison) { - reportError(GenericError{format("Metamethod '%s' must return type 'boolean'", it->second)}, expr->location); + if (!isBoolean(follow(*ret))) + { + reportError(GenericError{format("Metamethod '%s' must return a boolean", it->second)}, expr->location); + } + + return builtinTypes->booleanType; + } + else + { + return follow(*ret); } } - else if (!first(ftv->retTypes)) + else { - reportError(GenericError{format("Metamethod '%s' must return a value", it->second)}, expr->location); + if (isComparison) + { + reportError(GenericError{format("Metamethod '%s' must return a boolean", it->second)}, expr->location); + } + else + { + reportError(GenericError{format("Metamethod '%s' must return a value", it->second)}, expr->location); + } + + return builtinTypes->errorRecoveryType(); } } else @@ -1167,13 +1208,13 @@ struct TypeChecker2 reportError(CannotCallNonFunction{*mm}, expr->location); } - return; + return builtinTypes->errorRecoveryType(); } // If this is a string comparison, or a concatenation of strings, we // want to fall through to primitive behavior. else if (!isEquality && !(isStringOperation && (expr->op == AstExprBinary::Op::Concat || isComparison))) { - if (leftMt || rightMt) + if ((leftMt && !isString(leftType)) || (rightMt && !isString(rightType))) { if (isComparison) { @@ -1190,7 +1231,7 @@ struct TypeChecker2 expr->location); } - return; + return builtinTypes->errorRecoveryType(); } else if (!leftMt && !rightMt && (get(leftType) || get(rightType))) { @@ -1207,7 +1248,7 @@ struct TypeChecker2 expr->location); } - return; + return builtinTypes->errorRecoveryType(); } } } @@ -1223,34 +1264,44 @@ struct TypeChecker2 reportErrors(tryUnify(scope, expr->left->location, leftType, builtinTypes->numberType)); reportErrors(tryUnify(scope, expr->right->location, rightType, builtinTypes->numberType)); - break; + return builtinTypes->numberType; case AstExprBinary::Op::Concat: reportErrors(tryUnify(scope, expr->left->location, leftType, builtinTypes->stringType)); reportErrors(tryUnify(scope, expr->right->location, rightType, builtinTypes->stringType)); - break; + return builtinTypes->stringType; case AstExprBinary::Op::CompareGe: case AstExprBinary::Op::CompareGt: case AstExprBinary::Op::CompareLe: case AstExprBinary::Op::CompareLt: if (isNumber(leftType)) + { reportErrors(tryUnify(scope, expr->right->location, rightType, builtinTypes->numberType)); + return builtinTypes->numberType; + } else if (isString(leftType)) + { reportErrors(tryUnify(scope, expr->right->location, rightType, builtinTypes->stringType)); + return builtinTypes->stringType; + } else + { reportError(GenericError{format("Types '%s' and '%s' cannot be compared with relational operator %s", toString(leftType).c_str(), toString(rightType).c_str(), toString(expr->op).c_str())}, expr->location); - - break; + return builtinTypes->errorRecoveryType(); + } case AstExprBinary::Op::And: case AstExprBinary::Op::Or: case AstExprBinary::Op::CompareEq: case AstExprBinary::Op::CompareNe: - break; + // Ugly case: we don't care about this possibility, because a + // compound assignment will never exist with one of these operators. + return builtinTypes->anyType; default: // Unhandled AstExprBinary::Op possibility. LUAU_ASSERT(false); + return builtinTypes->errorRecoveryType(); } } diff --git a/Analysis/src/TypeInfer.cpp b/Analysis/src/TypeInfer.cpp index f31ea938..5c1ee388 100644 --- a/Analysis/src/TypeInfer.cpp +++ b/Analysis/src/TypeInfer.cpp @@ -16,9 +16,10 @@ #include "Luau/TopoSortStatements.h" #include "Luau/ToString.h" #include "Luau/ToString.h" -#include "Luau/TypePack.h" -#include "Luau/TypeUtils.h" #include "Luau/Type.h" +#include "Luau/TypePack.h" +#include "Luau/TypeReduction.h" +#include "Luau/TypeUtils.h" #include "Luau/VisitType.h" #include @@ -35,17 +36,16 @@ LUAU_FASTFLAG(LuauTypeNormalization2) LUAU_FASTFLAGVARIABLE(DebugLuauFreezeDuringUnification, false) LUAU_FASTFLAGVARIABLE(LuauReturnAnyInsteadOfICE, false) // Eventually removed as false. LUAU_FASTFLAGVARIABLE(DebugLuauSharedSelf, false) -LUAU_FASTFLAGVARIABLE(LuauNilIterator, false) LUAU_FASTFLAGVARIABLE(LuauUnknownAndNeverType, false) LUAU_FASTFLAGVARIABLE(LuauTypeInferMissingFollows, false) LUAU_FASTFLAGVARIABLE(LuauBinaryNeedsExpectedTypesToo, false) LUAU_FASTFLAGVARIABLE(LuauNeverTypesAndOperatorsInference, false) +LUAU_FASTFLAGVARIABLE(LuauScopelessModule, false) LUAU_FASTFLAGVARIABLE(LuauFollowInLvalueIndexCheck, false) LUAU_FASTFLAGVARIABLE(LuauReturnsFromCallsitesAreNotWidened, false) LUAU_FASTFLAGVARIABLE(LuauTryhardAnd, false) LUAU_FASTFLAG(LuauInstantiateInSubtyping) LUAU_FASTFLAGVARIABLE(LuauCompleteVisitor, false) -LUAU_FASTFLAGVARIABLE(LuauOptionalNextKey, false) LUAU_FASTFLAGVARIABLE(LuauReportShadowedTypeAlias, false) LUAU_FASTFLAGVARIABLE(LuauBetterMessagingOnCountMismatch, false) LUAU_FASTFLAGVARIABLE(LuauIntersectionTestForEquality, false) @@ -276,6 +276,7 @@ ModulePtr TypeChecker::checkWithoutRecursionCheck(const SourceModule& module, Mo LUAU_TIMETRACE_ARGUMENT("module", module.name.c_str()); currentModule.reset(new Module); + currentModule->reduction = std::make_unique(NotNull{¤tModule->internalTypes}, builtinTypes, NotNull{iceHandler}); currentModule->type = module.type; currentModule->allocator = module.allocator; currentModule->names = module.names; @@ -1136,7 +1137,8 @@ void TypeChecker::check(const ScopePtr& scope, const AstStatLocal& local) const Name name{local.vars.data[i]->name.value}; if (ModulePtr module = resolver->getModule(moduleInfo->name)) - scope->importedTypeBindings[name] = module->getModuleScope()->exportedTypeBindings; + scope->importedTypeBindings[name] = + FFlag::LuauScopelessModule ? module->exportedTypeBindings : module->getModuleScope()->exportedTypeBindings; // In non-strict mode we force the module type on the variable, in strict mode it is already unified if (isNonstrictMode()) @@ -1248,8 +1250,7 @@ void TypeChecker::check(const ScopePtr& scope, const AstStatForIn& forin) iterTy = instantiate(scope, checkExpr(scope, *firstValue).type, firstValue->location); } - if (FFlag::LuauNilIterator) - iterTy = stripFromNilAndReport(iterTy, firstValue->location); + iterTy = stripFromNilAndReport(iterTy, firstValue->location); if (std::optional iterMM = findMetatableEntry(iterTy, "__iter", firstValue->location, /* addErrors= */ true)) { @@ -1334,62 +1335,41 @@ void TypeChecker::check(const ScopePtr& scope, const AstStatForIn& forin) reportErrors(state.errors); } - if (FFlag::LuauOptionalNextKey) + TypePackId retPack = iterFunc->retTypes; + + if (forin.values.size >= 2) { - TypePackId retPack = iterFunc->retTypes; + AstArray arguments{forin.values.data + 1, forin.values.size - 1}; - if (forin.values.size >= 2) - { - AstArray arguments{forin.values.data + 1, forin.values.size - 1}; + Position start = firstValue->location.begin; + Position end = values[forin.values.size - 1]->location.end; + AstExprCall exprCall{Location(start, end), firstValue, arguments, /* self= */ false, Location()}; - Position start = firstValue->location.begin; - Position end = values[forin.values.size - 1]->location.end; - AstExprCall exprCall{Location(start, end), firstValue, arguments, /* self= */ false, Location()}; - - retPack = checkExprPack(scope, exprCall).type; - } - - // We need to remove 'nil' from the set of options of the first return value - // Because for loop stops when it gets 'nil', this result is never actually assigned to the first variable - if (std::optional fty = first(retPack); fty && !varTypes.empty()) - { - TypeId keyTy = follow(*fty); - - if (get(keyTy)) - { - if (std::optional ty = tryStripUnionFromNil(keyTy)) - keyTy = *ty; - } - - unify(keyTy, varTypes.front(), scope, forin.location); - - // We have already handled the first variable type, make it match in the pack check - varTypes.front() = *fty; - } - - TypePackId varPack = addTypePack(TypePackVar{TypePack{varTypes, freshTypePack(scope)}}); - - unify(retPack, varPack, scope, forin.location); + retPack = checkExprPack(scope, exprCall).type; } - else + + // We need to remove 'nil' from the set of options of the first return value + // Because for loop stops when it gets 'nil', this result is never actually assigned to the first variable + if (std::optional fty = first(retPack); fty && !varTypes.empty()) { - TypePackId varPack = addTypePack(TypePackVar{TypePack{varTypes, freshTypePack(scope)}}); + TypeId keyTy = follow(*fty); - if (forin.values.size >= 2) + if (get(keyTy)) { - AstArray arguments{forin.values.data + 1, forin.values.size - 1}; - - Position start = firstValue->location.begin; - Position end = values[forin.values.size - 1]->location.end; - AstExprCall exprCall{Location(start, end), firstValue, arguments, /* self= */ false, Location()}; - - TypePackId retPack = checkExprPack(scope, exprCall).type; - unify(retPack, varPack, scope, forin.location); + if (std::optional ty = tryStripUnionFromNil(keyTy)) + keyTy = *ty; } - else - unify(iterFunc->retTypes, varPack, scope, forin.location); + + unify(keyTy, varTypes.front(), scope, forin.location); + + // We have already handled the first variable type, make it match in the pack check + varTypes.front() = *fty; } + TypePackId varPack = addTypePack(TypePackVar{TypePack{varTypes, freshTypePack(scope)}}); + + unify(retPack, varPack, scope, forin.location); + check(loopScope, *forin.body); } @@ -4685,7 +4665,7 @@ TypeId TypeChecker::checkRequire(const ScopePtr& scope, const ModuleInfo& module return errorRecoveryType(scope); } - TypePackId modulePack = module->getModuleScope()->returnType; + TypePackId modulePack = FFlag::LuauScopelessModule ? module->returnType : module->getModuleScope()->returnType; if (get(modulePack)) return errorRecoveryType(scope); diff --git a/Analysis/src/TypeReduction.cpp b/Analysis/src/TypeReduction.cpp new file mode 100644 index 00000000..db79debf --- /dev/null +++ b/Analysis/src/TypeReduction.cpp @@ -0,0 +1,838 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#include "Luau/TypeReduction.h" + +#include "Luau/Common.h" +#include "Luau/Error.h" +#include "Luau/RecursionCounter.h" + +#include +#include + +LUAU_FASTINTVARIABLE(LuauTypeReductionCartesianProductLimit, 100'000) +LUAU_FASTINTVARIABLE(LuauTypeReductionRecursionLimit, 700) +LUAU_FASTFLAGVARIABLE(DebugLuauDontReduceTypes, false) + +namespace Luau +{ + +namespace +{ + +struct RecursionGuard : RecursionLimiter +{ + std::deque* seen; + + RecursionGuard(int* count, int limit, std::deque* seen) + : RecursionLimiter(count, limit) + , seen(seen) + { + // count has been incremented, which should imply that seen has already had an element pushed in. + LUAU_ASSERT(size_t(*count) == seen->size()); + } + + ~RecursionGuard() + { + LUAU_ASSERT(!seen->empty()); // It is UB to pop_back() on an empty deque. + seen->pop_back(); + } +}; + +template +std::pair get2(const Thing& one, const Thing& two) +{ + const A* a = get(one); + const B* b = get(two); + return a && b ? std::make_pair(a, b) : std::make_pair(nullptr, nullptr); +} + +struct TypeReducer +{ + NotNull arena; + NotNull builtinTypes; + NotNull handle; + + TypeId reduce(TypeId ty); + TypePackId reduce(TypePackId tp); + + std::optional intersectionType(TypeId left, TypeId right); + std::optional unionType(TypeId left, TypeId right); + TypeId tableType(TypeId ty); + TypeId functionType(TypeId ty); + TypeId negationType(TypeId ty); + + std::deque seen; + int depth = 0; + + RecursionGuard guard(TypeId ty); + RecursionGuard guard(TypePackId tp); + + std::unordered_map copies; + + template + LUAU_NOINLINE std::pair copy(TypeId ty, const T* t) + { + if (auto it = copies.find(ty); it != copies.end()) + return {it->second, getMutable(it->second)}; + + TypeId copiedTy = arena->addType(*t); + copies[ty] = copiedTy; + return {copiedTy, getMutable(copiedTy)}; + } + + using Folder = std::optional (TypeReducer::*)(TypeId, TypeId); + + template + void foldl_impl(Iter it, Iter endIt, Folder f, NotNull> result) + { + while (it != endIt) + { + bool replaced = false; + TypeId currentTy = reduce(*it); + RecursionGuard rg = guard(*it); + + // We're hitting a case where the `currentTy` returned a type that's the same as `T`. + // e.g. `(string?) & ~(false | nil)` became `(string?) & (~false & ~nil)` but the current iterator we're consuming doesn't know this. + // We will need to recurse and traverse that first. + if (auto t = get(currentTy)) + { + foldl_impl(begin(t), end(t), f, result); + ++it; + continue; + } + + auto resultIt = result->begin(); + while (resultIt != result->end()) + { + TypeId& ty = *resultIt; + + std::optional reduced = (this->*f)(ty, currentTy); + if (reduced && replaced) + { + // We want to erase any other elements that occurs after the first replacement too. + // e.g. `"a" | "b" | string` where `"a"` and `"b"` is in the `result` vector, then `string` replaces both `"a"` and `"b"`. + // If we don't erase redundant elements, `"b"` may be kept or be replaced by `string`, leaving us with `string | string`. + resultIt = result->erase(resultIt); + } + else if (reduced && !replaced) + { + ++resultIt; + replaced = true; + ty = *reduced; + } + else + { + ++resultIt; + continue; + } + } + + if (!replaced) + result->push_back(currentTy); + + ++it; + } + } + + template + TypeId foldl(Iter it, Iter endIt, Folder f) + { + std::vector result; + foldl_impl(it, endIt, f, NotNull{&result}); + if (result.size() == 1) + return result[0]; + else + return arena->addType(T{std::move(result)}); + } +}; + +TypeId TypeReducer::reduce(TypeId ty) +{ + ty = follow(ty); + + if (std::find(seen.begin(), seen.end(), ty) != seen.end()) + return ty; + + RecursionGuard rg = guard(ty); + + if (auto i = get(ty)) + return foldl(begin(i), end(i), &TypeReducer::intersectionType); + else if (auto u = get(ty)) + return foldl(begin(u), end(u), &TypeReducer::unionType); + else if (get(ty) || get(ty)) + return tableType(ty); + else if (get(ty)) + return functionType(ty); + else if (auto n = get(ty)) + return negationType(follow(n->ty)); + else + return ty; +} + +TypePackId TypeReducer::reduce(TypePackId tp) +{ + tp = follow(tp); + + if (std::find(seen.begin(), seen.end(), tp) != seen.end()) + return tp; + + RecursionGuard rg = guard(tp); + + TypePackIterator it = begin(tp); + + std::vector head; + while (it != end(tp)) + { + head.push_back(reduce(*it)); + ++it; + } + + std::optional tail = it.tail(); + if (tail) + { + if (auto vtp = get(follow(*it.tail()))) + tail = arena->addTypePack(VariadicTypePack{reduce(vtp->ty), vtp->hidden}); + } + + return arena->addTypePack(TypePack{std::move(head), tail}); +} + +std::optional TypeReducer::intersectionType(TypeId left, TypeId right) +{ + LUAU_ASSERT(!get(left)); + LUAU_ASSERT(!get(right)); + + if (get(left)) + return left; // never & T ~ never + else if (get(right)) + return right; // T & never ~ never + else if (get(left)) + return right; // unknown & T ~ T + else if (get(right)) + return left; // T & unknown ~ T + else if (get(left)) + return right; // any & T ~ T + else if (get(right)) + return left; // T & any ~ T + else if (get(left)) + return std::nullopt; // error & T ~ error & T + else if (get(right)) + return std::nullopt; // T & error ~ T & error + else if (auto ut = get(left)) + { + std::vector options; + for (TypeId option : ut) + { + if (auto result = intersectionType(option, right)) + options.push_back(*result); + else + options.push_back(arena->addType(IntersectionType{{option, right}})); + } + + return foldl(begin(options), end(options), &TypeReducer::unionType); // (A | B) & T ~ (A & T) | (B & T) + } + else if (get(right)) + return intersectionType(right, left); // T & (A | B) ~ (A | B) & T + else if (auto [p1, p2] = get2(left, right); p1 && p2) + { + if (p1->type == p2->type) + return left; // P1 & P2 ~ P1 iff P1 == P2 + else + return builtinTypes->neverType; // P1 & P2 ~ never iff P1 != P2 + } + else if (auto [p, s] = get2(left, right); p && s) + { + if (p->type == PrimitiveType::String && get(s)) + return right; // string & "A" ~ "A" + else if (p->type == PrimitiveType::Boolean && get(s)) + return right; // boolean & true ~ true + else + return builtinTypes->neverType; // string & true ~ never + } + else if (auto [s, p] = get2(left, right); s && p) + return intersectionType(right, left); // S & P ~ P & S + else if (auto [p, f] = get2(left, right); p && f) + { + if (p->type == PrimitiveType::Function) + return right; // function & () -> () ~ () -> () + else + return builtinTypes->neverType; // string & () -> () ~ never + } + else if (auto [f, p] = get2(left, right); f && p) + return intersectionType(right, left); // () -> () & P ~ P & () -> () + else if (auto [s1, s2] = get2(left, right); s1 && s2) + { + if (*s1 == *s2) + return left; // "a" & "a" ~ "a" + else + return builtinTypes->neverType; // "a" & "b" ~ never + } + else if (auto [c1, c2] = get2(left, right); c1 && c2) + { + if (isSubclass(c1, c2)) + return left; // Derived & Base ~ Derived + else if (isSubclass(c2, c1)) + return right; // Base & Derived ~ Derived + else + return builtinTypes->neverType; // Base & Unrelated ~ never + } + else if (auto [f1, f2] = get2(left, right); f1 && f2) + { + if (std::find(seen.begin(), seen.end(), left) != seen.end()) + return std::nullopt; + else if (std::find(seen.begin(), seen.end(), right) != seen.end()) + return std::nullopt; + + return std::nullopt; // TODO + } + else if (auto [t1, t2] = get2(left, right); t1 && t2) + { + if (t1->state == TableState::Free || t2->state == TableState::Free) + return std::nullopt; // '{ x: T } & { x: U } ~ '{ x: T } & { x: U } + else if (t1->state == TableState::Generic || t2->state == TableState::Generic) + return std::nullopt; // '{ x: T } & { x: U } ~ '{ x: T } & { x: U } + + if (std::find(seen.begin(), seen.end(), left) != seen.end()) + return std::nullopt; + else if (std::find(seen.begin(), seen.end(), right) != seen.end()) + return std::nullopt; + + TypeId resultTy = arena->addType(TableType{}); + TableType* table = getMutable(resultTy); + table->state = t1->state == TableState::Sealed || t2->state == TableState::Sealed ? TableState::Sealed : TableState::Unsealed; + + for (const auto& [name, prop] : t1->props) + { + // TODO: when t1 has properties, we should also intersect that with the indexer in t2 if it exists, + // even if we have the corresponding property in the other one. + if (auto other = t2->props.find(name); other != t2->props.end()) + { + std::vector parts{prop.type, other->second.type}; + TypeId propTy = foldl(begin(parts), end(parts), &TypeReducer::intersectionType); + if (get(propTy)) + return builtinTypes->neverType; // { p : string } & { p : number } ~ { p : string & number } ~ { p : never } ~ never + else + table->props[name] = {propTy}; // { p : string } & { p : ~"a" } ~ { p : string & ~"a" } + } + else + table->props[name] = prop; // { p : string } & {} ~ { p : string } + } + + for (const auto& [name, prop] : t2->props) + { + // TODO: And vice versa, t2 properties against t1 indexer if it exists, + // even if we have the corresponding property in the other one. + if (!t1->props.count(name)) + table->props[name] = prop; // {} & { p : string } ~ { p : string } + } + + if (t1->indexer && t2->indexer) + { + std::vector keyParts{t1->indexer->indexType, t2->indexer->indexType}; + TypeId keyTy = foldl(begin(keyParts), end(keyParts), &TypeReducer::intersectionType); + if (get(keyTy)) + return builtinTypes->neverType; // { [string]: _ } & { [number]: _ } ~ { [string & number]: _ } ~ { [never]: _ } ~ never + + std::vector valueParts{t1->indexer->indexResultType, t2->indexer->indexResultType}; + TypeId valueTy = foldl(begin(valueParts), end(valueParts), &TypeReducer::intersectionType); + if (get(valueTy)) + return builtinTypes->neverType; // { [_]: string } & { [_]: number } ~ { [_]: string & number } ~ { [_]: never } ~ never + + table->indexer = TableIndexer{keyTy, valueTy}; + } + else if (t1->indexer) + table->indexer = t1->indexer; // { [number]: boolean } & { p : string } ~ { p : string, [number]: boolean } + else if (t2->indexer) + table->indexer = t2->indexer; // { p : string } & { [number]: boolean } ~ { p : string, [number]: boolean } + + return resultTy; + } + else if (auto [mt, tt] = get2(left, right); mt && tt) + return std::nullopt; // TODO + else if (auto [tt, mt] = get2(left, right); tt && mt) + return intersectionType(right, left); // T & M ~ M & T + else if (auto [m1, m2] = get2(left, right); m1 && m2) + return std::nullopt; // TODO + else if (auto nl = get(left)) + { + // These should've been reduced already. + TypeId nlTy = follow(nl->ty); + LUAU_ASSERT(!get(nlTy)); + LUAU_ASSERT(!get(nlTy)); + LUAU_ASSERT(!get(nlTy)); + LUAU_ASSERT(!get(nlTy)); + LUAU_ASSERT(!get(nlTy)); + + if (auto [np, p] = get2(nlTy, right); np && p) + { + if (np->type == p->type) + return builtinTypes->neverType; // ~P1 & P2 ~ never iff P1 == P2 + else + return right; // ~P1 & P2 ~ P2 iff P1 != P2 + } + else if (auto [ns, s] = get2(nlTy, right); ns && s) + { + if (*ns == *s) + return builtinTypes->neverType; // ~"A" & "A" ~ never + else + return right; // ~"A" & "B" ~ "B" + } + else if (auto [ns, p] = get2(nlTy, right); ns && p) + { + if (get(ns) && p->type == PrimitiveType::String) + return std::nullopt; // ~"A" & string ~ ~"A" & string + else if (get(ns) && p->type == PrimitiveType::Boolean) + { + // Because booleans contain a fixed amount of values (2), we can do something cooler with this one. + const BooleanSingleton* b = get(ns); + return arena->addType(SingletonType{BooleanSingleton{!b->value}}); // ~false & boolean ~ true + } + else + return right; // ~"A" & number ~ number + } + else if (auto [np, s] = get2(nlTy, right); np && s) + { + if (np->type == PrimitiveType::String && get(s)) + return builtinTypes->neverType; // ~string & "A" ~ never + else if (np->type == PrimitiveType::Boolean && get(s)) + return builtinTypes->neverType; // ~boolean & true ~ never + else + return right; // ~P & "A" ~ "A" + } + else if (auto [np, f] = get2(nlTy, right); np && f) + { + if (np->type == PrimitiveType::Function) + return builtinTypes->neverType; // ~function & () -> () ~ never + else + return right; // ~string & () -> () ~ () -> () + } + else if (auto [nc, c] = get2(nlTy, right); nc && c) + { + if (isSubclass(nc, c)) + return std::nullopt; // ~Derived & Base ~ ~Derived & Base + else if (isSubclass(c, nc)) + return builtinTypes->neverType; // ~Base & Derived ~ never + else + return right; // ~Base & Unrelated ~ Unrelated + } + else + return std::nullopt; // TODO + } + else if (get(right)) + return intersectionType(right, left); // T & ~U ~ ~U & T + else + return builtinTypes->neverType; // for all T and U except the ones handled above, T & U ~ never +} + +std::optional TypeReducer::unionType(TypeId left, TypeId right) +{ + LUAU_ASSERT(!get(left)); + LUAU_ASSERT(!get(right)); + + if (get(left)) + return right; // never | T ~ T + else if (get(right)) + return left; // T | never ~ T + else if (get(left)) + return left; // unknown | T ~ unknown + else if (get(right)) + return right; // T | unknown ~ unknown + else if (get(left)) + return left; // any | T ~ any + else if (get(right)) + return right; // T | any ~ any + else if (get(left)) + return std::nullopt; // error | T ~ error | T + else if (get(right)) + return std::nullopt; // T | error ~ T | error + else if (auto [p1, p2] = get2(left, right); p1 && p2) + { + if (p1->type == p2->type) + return left; // P1 | P2 ~ P1 iff P1 == P2 + else + return std::nullopt; // P1 | P2 ~ P1 | P2 iff P1 != P2 + } + else if (auto [p, s] = get2(left, right); p && s) + { + if (p->type == PrimitiveType::String && get(s)) + return left; // string | "A" ~ string + else if (p->type == PrimitiveType::Boolean && get(s)) + return left; // boolean | true ~ boolean + else + return std::nullopt; // string | true ~ string | true + } + else if (auto [s, p] = get2(left, right); s && p) + return unionType(right, left); // S | P ~ P | S + else if (auto [p, f] = get2(left, right); p && f) + { + if (p->type == PrimitiveType::Function) + return left; // function | () -> () ~ function + else + return std::nullopt; // P | () -> () ~ P | () -> () + } + else if (auto [f, p] = get2(left, right); f && p) + return unionType(right, left); // () -> () | P ~ P | () -> () + else if (auto [s1, s2] = get2(left, right); s1 && s2) + { + if (*s1 == *s2) + return left; // "a" | "a" ~ "a" + else + return std::nullopt; // "a" | "b" ~ "a" | "b" + } + else if (auto [c1, c2] = get2(left, right); c1 && c2) + { + if (isSubclass(c1, c2)) + return right; // Derived | Base ~ Base + else if (isSubclass(c2, c1)) + return left; // Base | Derived ~ Base + else + return std::nullopt; // Base | Unrelated ~ Base | Unrelated + } + else if (auto [nt, it] = get2(left, right); nt && it) + { + std::vector parts; + for (TypeId option : it) + { + if (auto result = unionType(left, option)) + parts.push_back(*result); + else + { + // TODO: does there exist a reduced form such that `~T | A` hasn't already reduced it, if `A & B` is irreducible? + // I want to say yes, but I can't generate a case that hits this code path. + parts.push_back(arena->addType(UnionType{{left, option}})); + } + } + + return foldl(begin(parts), end(parts), &TypeReducer::intersectionType); // ~T | (A & B) ~ (~T | A) & (~T | B) + } + else if (auto [it, nt] = get2(left, right); it && nt) + return unionType(right, left); // (A & B) | ~T ~ ~T | (A & B) + else if (auto [nl, nr] = get2(left, right); nl && nr) + { + // These should've been reduced already. + TypeId nlTy = follow(nl->ty); + TypeId nrTy = follow(nr->ty); + LUAU_ASSERT(!get(nlTy) && !get(nrTy)); + LUAU_ASSERT(!get(nlTy) && !get(nrTy)); + LUAU_ASSERT(!get(nlTy) && !get(nrTy)); + LUAU_ASSERT(!get(nlTy) && !get(nrTy)); + LUAU_ASSERT(!get(nlTy) && !get(nrTy)); + + if (auto [npl, npr] = get2(nlTy, nrTy); npl && npr) + { + if (npl->type == npr->type) + return left; // ~P1 | ~P2 ~ ~P1 iff P1 == P2 + else + return builtinTypes->unknownType; // ~P1 | ~P2 ~ ~P1 iff P1 != P2 + } + else if (auto [nsl, nsr] = get2(nlTy, nrTy); nsl && nsr) + { + if (*nsl == *nsr) + return left; // ~"A" | ~"A" ~ ~"A" + else + return builtinTypes->unknownType; // ~"A" | ~"B" ~ unknown + } + else if (auto [ns, np] = get2(nlTy, nrTy); ns && np) + { + if (get(ns) && np->type == PrimitiveType::String) + return left; // ~"A" | ~string ~ ~"A" + else if (get(ns) && np->type == PrimitiveType::Boolean) + return left; // ~false | ~boolean ~ ~false + else + return builtinTypes->unknownType; // ~"A" | ~P ~ unknown + } + else if (auto [np, ns] = get2(nlTy, nrTy); np && ns) + return unionType(right, left); // ~P | ~S ~ ~S | ~P + else + return std::nullopt; // TODO! + } + else if (auto nl = get(left)) + { + // These should've been reduced already. + TypeId nlTy = follow(nl->ty); + LUAU_ASSERT(!get(nlTy)); + LUAU_ASSERT(!get(nlTy)); + LUAU_ASSERT(!get(nlTy)); + LUAU_ASSERT(!get(nlTy)); + LUAU_ASSERT(!get(nlTy)); + + if (auto [np, p] = get2(nlTy, right); np && p) + { + if (np->type == p->type) + return builtinTypes->unknownType; // ~P1 | P2 ~ unknown iff P1 == P2 + else + return left; // ~P1 | P2 ~ ~P1 iff P1 != P2 + } + else if (auto [ns, s] = get2(nlTy, right); ns && s) + { + if (*ns == *s) + return builtinTypes->unknownType; // ~"A" | "A" ~ unknown + else + return left; // ~"A" | "B" ~ ~"A" + } + else if (auto [ns, p] = get2(nlTy, right); ns && p) + { + if (get(ns) && p->type == PrimitiveType::String) + return builtinTypes->unknownType; // ~"A" | string ~ unknown + else if (get(ns) && p->type == PrimitiveType::Boolean) + return builtinTypes->unknownType; // ~false | boolean ~ unknown + else + return left; // ~"A" | T ~ ~"A" + } + else if (auto [np, s] = get2(nlTy, right); np && s) + { + if (np->type == PrimitiveType::String && get(s)) + return std::nullopt; // ~string | "A" ~ ~string | "A" + else if (np->type == PrimitiveType::Boolean && get(s)) + { + const BooleanSingleton* b = get(s); + return negationType(arena->addType(SingletonType{BooleanSingleton{!b->value}})); // ~boolean | false ~ ~true + } + else + return left; // ~P | "A" ~ ~P + } + else if (auto [nc, c] = get2(nlTy, right); nc && c) + { + if (isSubclass(nc, c)) + return builtinTypes->unknownType; // ~Derived | Base ~ unknown + else if (isSubclass(c, nc)) + return std::nullopt; // ~Base | Derived ~ ~Base | Derived + else + return left; // ~Base | Unrelated ~ ~Base + } + else + return std::nullopt; // TODO + } + else if (get(right)) + return unionType(right, left); // T | ~U ~ ~U | T + else + return std::nullopt; // for all T and U except the ones handled above, T | U ~ T | U +} + +TypeId TypeReducer::tableType(TypeId ty) +{ + RecursionGuard rg = guard(ty); + + if (auto mt = get(ty)) + { + auto [copiedTy, copied] = copy(ty, mt); + copied->table = reduce(mt->table); + copied->metatable = reduce(mt->metatable); + return copiedTy; + } + else if (auto tt = get(ty)) + { + auto [copiedTy, copied] = copy(ty, tt); + + for (auto& [name, prop] : copied->props) + prop.type = reduce(prop.type); + + if (auto& indexer = copied->indexer) + { + indexer->indexType = reduce(indexer->indexType); + indexer->indexResultType = reduce(indexer->indexResultType); + } + + for (TypeId& ty : copied->instantiatedTypeParams) + ty = reduce(ty); + + for (TypePackId& tp : copied->instantiatedTypePackParams) + tp = reduce(tp); + + return copiedTy; + } + else + handle->ice("Unexpected type in TypeReducer::tableType"); +} + +TypeId TypeReducer::functionType(TypeId ty) +{ + RecursionGuard rg = guard(ty); + + const FunctionType* f = get(ty); + if (!f) + handle->ice("TypeReducer::reduce expects a FunctionType"); + + // TODO: once we have bounded quantification, we need to be able to reduce the generic bounds. + auto [copiedTy, copied] = copy(ty, f); + copied->argTypes = reduce(f->argTypes); + copied->retTypes = reduce(f->retTypes); + return copiedTy; +} + +TypeId TypeReducer::negationType(TypeId ty) +{ + RecursionGuard rg = guard(ty); + + if (auto nn = get(ty)) + return nn->ty; // ~~T ~ T + else if (get(ty)) + return builtinTypes->unknownType; // ~never ~ unknown + else if (get(ty)) + return builtinTypes->neverType; // ~unknown ~ never + else if (get(ty)) + return builtinTypes->anyType; // ~any ~ any + else if (auto ni = get(ty)) + { + std::vector options; + for (TypeId part : ni) + options.push_back(negationType(part)); + return foldl(begin(options), end(options), &TypeReducer::unionType); // ~(T & U) ~ (~T | ~U) + } + else if (auto nu = get(ty)) + { + std::vector parts; + for (TypeId option : nu) + parts.push_back(negationType(option)); + return foldl(begin(parts), end(parts), &TypeReducer::intersectionType); // ~(T | U) ~ (~T & ~U) + } + else + return arena->addType(NegationType{ty}); // for all T except the ones handled above, ~T ~ ~T +} + +RecursionGuard TypeReducer::guard(TypeId ty) +{ + seen.push_back(ty); + return RecursionGuard{&depth, FInt::LuauTypeReductionRecursionLimit, &seen}; +} + +RecursionGuard TypeReducer::guard(TypePackId tp) +{ + seen.push_back(tp); + return RecursionGuard{&depth, FInt::LuauTypeReductionRecursionLimit, &seen}; +} + +} // namespace + +TypeReduction::TypeReduction(NotNull arena, NotNull builtinTypes, NotNull handle) + : arena(arena) + , builtinTypes(builtinTypes) + , handle(handle) +{ +} + +std::optional TypeReduction::reduce(TypeId ty) +{ + if (auto found = cachedTypes.find(ty)) + return *found; + + if (auto reduced = reduceImpl(ty)) + { + cachedTypes[ty] = *reduced; + return *reduced; + } + + return std::nullopt; +} + +std::optional TypeReduction::reduce(TypePackId tp) +{ + if (auto found = cachedTypePacks.find(tp)) + return *found; + + if (auto reduced = reduceImpl(tp)) + { + cachedTypePacks[tp] = *reduced; + return *reduced; + } + + return std::nullopt; +} + +std::optional TypeReduction::reduceImpl(TypeId ty) +{ + if (FFlag::DebugLuauDontReduceTypes) + return ty; + + if (hasExceededCartesianProductLimit(ty)) + return std::nullopt; + + try + { + TypeReducer reducer{arena, builtinTypes, handle}; + return reducer.reduce(ty); + } + catch (const RecursionLimitException&) + { + return std::nullopt; + } +} + +std::optional TypeReduction::reduceImpl(TypePackId tp) +{ + if (FFlag::DebugLuauDontReduceTypes) + return tp; + + if (hasExceededCartesianProductLimit(tp)) + return std::nullopt; + + try + { + TypeReducer reducer{arena, builtinTypes, handle}; + return reducer.reduce(tp); + } + catch (const RecursionLimitException&) + { + return std::nullopt; + } +} + +std::optional TypeReduction::reduce(const TypeFun& fun) +{ + if (FFlag::DebugLuauDontReduceTypes) + return fun; + + // TODO: once we have bounded quantification, we need to be able to reduce the generic bounds. + if (auto reducedTy = reduce(fun.type)) + return TypeFun{fun.typeParams, fun.typePackParams, *reducedTy}; + + return std::nullopt; +} + +size_t TypeReduction::cartesianProductSize(TypeId ty) const +{ + ty = follow(ty); + + auto it = get(follow(ty)); + if (!it) + return 1; + + return std::accumulate(begin(it), end(it), size_t(1), [](size_t acc, TypeId ty) { + if (auto ut = get(ty)) + return acc * std::distance(begin(ut), end(ut)); + else if (get(ty)) + return acc * 0; + else + return acc * 1; + }); +} + +bool TypeReduction::hasExceededCartesianProductLimit(TypeId ty) const +{ + return cartesianProductSize(ty) >= size_t(FInt::LuauTypeReductionCartesianProductLimit); +} + +bool TypeReduction::hasExceededCartesianProductLimit(TypePackId tp) const +{ + TypePackIterator it = begin(tp); + + while (it != end(tp)) + { + if (hasExceededCartesianProductLimit(*it)) + return true; + + ++it; + } + + if (auto tail = it.tail()) + { + if (auto vtp = get(follow(*tail))) + { + if (hasExceededCartesianProductLimit(vtp->ty)) + return true; + } + } + + return false; +} + +} // namespace Luau diff --git a/Compiler/src/Compiler.cpp b/Compiler/src/Compiler.cpp index b4dea4f5..8a1e80fc 100644 --- a/Compiler/src/Compiler.cpp +++ b/Compiler/src/Compiler.cpp @@ -27,6 +27,7 @@ LUAU_FASTINTVARIABLE(LuauCompileInlineDepth, 5) LUAU_FASTFLAG(LuauInterpolatedStringBaseSupport) LUAU_FASTFLAGVARIABLE(LuauMultiAssignmentConflictFix, false) +LUAU_FASTFLAGVARIABLE(LuauSelfAssignmentSkip, false) namespace Luau { @@ -2027,7 +2028,9 @@ struct Compiler // note: this can't check expr->upvalue because upvalues may be upgraded to locals during inlining if (int reg = getExprLocalReg(expr); reg >= 0) { - bytecode.emitABC(LOP_MOVE, target, uint8_t(reg), 0); + // Optimization: we don't need to move if target happens to be in the same register + if (!FFlag::LuauSelfAssignmentSkip || options.optimizationLevel == 0 || target != reg) + bytecode.emitABC(LOP_MOVE, target, uint8_t(reg), 0); } else { diff --git a/Sources.cmake b/Sources.cmake index 437ff993..87d76bf3 100644 --- a/Sources.cmake +++ b/Sources.cmake @@ -146,6 +146,7 @@ target_sources(Luau.Analysis PRIVATE Analysis/include/Luau/TypedAllocator.h Analysis/include/Luau/TypeInfer.h Analysis/include/Luau/TypePack.h + Analysis/include/Luau/TypeReduction.h Analysis/include/Luau/TypeUtils.h Analysis/include/Luau/Type.h Analysis/include/Luau/Unifiable.h @@ -195,6 +196,7 @@ target_sources(Luau.Analysis PRIVATE Analysis/src/TypedAllocator.cpp Analysis/src/TypeInfer.cpp Analysis/src/TypePack.cpp + Analysis/src/TypeReduction.cpp Analysis/src/TypeUtils.cpp Analysis/src/Type.cpp Analysis/src/Unifiable.cpp @@ -364,6 +366,7 @@ if(TARGET Luau.UnitTest) tests/TypeInfer.unionTypes.test.cpp tests/TypeInfer.unknownnever.test.cpp tests/TypePack.test.cpp + tests/TypeReduction.test.cpp tests/TypeVar.test.cpp tests/Variant.test.cpp tests/VisitType.test.cpp diff --git a/tests/Autocomplete.test.cpp b/tests/Autocomplete.test.cpp index 10582947..f241963a 100644 --- a/tests/Autocomplete.test.cpp +++ b/tests/Autocomplete.test.cpp @@ -3331,4 +3331,36 @@ TEST_CASE_FIXTURE(ACFixture, "globals_are_order_independent") CHECK(ac.entryMap.count("abc1")); } +TEST_CASE_FIXTURE(ACFixture, "type_reduction_is_hooked_up_to_autocomplete") +{ + check(R"( + type T = { x: (number & string)? } + + function f(thingamabob: T) + thingamabob.@1 + end + + function g(thingamabob: T) + thingama@2 + end + )"); + + ToStringOptions opts; + opts.exhaustive = true; + + auto ac1 = autocomplete('1'); + REQUIRE(ac1.entryMap.count("x")); + std::optional ty1 = ac1.entryMap.at("x").type; + REQUIRE(ty1); + CHECK("(number & string)?" == toString(*ty1, opts)); + // CHECK("nil" == toString(*ty1, opts)); + + auto ac2 = autocomplete('2'); + REQUIRE(ac2.entryMap.count("thingamabob")); + std::optional ty2 = ac2.entryMap.at("thingamabob").type; + REQUIRE(ty2); + CHECK("{| x: (number & string)? |}" == toString(*ty2, opts)); + // CHECK("{| x: nil |}" == toString(*ty2, opts)); +} + TEST_SUITE_END(); diff --git a/tests/Compiler.test.cpp b/tests/Compiler.test.cpp index d2cf0ae8..1a606126 100644 --- a/tests/Compiler.test.cpp +++ b/tests/Compiler.test.cpp @@ -1025,6 +1025,8 @@ L0: RETURN R0 0 TEST_CASE("AndOr") { + ScopedFastFlag luauSelfAssignmentSkip{"LuauSelfAssignmentSkip", true}; + // codegen for constant, local, global for and CHECK_EQ("\n" + compileFunction0("local a = 1 a = a and 2 return a"), R"( LOADN R0 1 @@ -1079,7 +1081,6 @@ RETURN R0 1 // note: `a = a` assignment is to disable constant folding for testing purposes CHECK_EQ("\n" + compileFunction0("local a = 1 a = a b = 2 local c = a and b return c"), R"( LOADN R0 1 -MOVE R0 R0 LOADN R1 2 SETGLOBAL R1 K0 MOVE R1 R0 @@ -1090,7 +1091,6 @@ L0: RETURN R1 1 CHECK_EQ("\n" + compileFunction0("local a = 1 a = a b = 2 local c = a or b return c"), R"( LOADN R0 1 -MOVE R0 R0 LOADN R1 2 SETGLOBAL R1 K0 MOVE R1 R0 @@ -2260,6 +2260,8 @@ L1: RETURN R3 -1 TEST_CASE("UpvaluesLoopsBytecode") { + ScopedFastFlag luauSelfAssignmentSkip{"LuauSelfAssignmentSkip", true}; + CHECK_EQ("\n" + compileFunction(R"( function test() for i=1,10 do @@ -2279,7 +2281,6 @@ LOADN R0 10 LOADN R1 1 FORNPREP R0 L2 L0: MOVE R3 R2 -MOVE R3 R3 GETIMPORT R4 1 NEWCLOSURE R5 P0 CAPTURE REF R3 @@ -2312,8 +2313,7 @@ GETIMPORT R0 1 GETIMPORT R1 3 CALL R0 1 3 FORGPREP_INEXT R0 L2 -L0: MOVE R3 R3 -GETIMPORT R5 5 +L0: GETIMPORT R5 5 NEWCLOSURE R6 P0 CAPTURE REF R3 CALL R5 1 0 @@ -5159,6 +5159,8 @@ RETURN R1 1 TEST_CASE("InlineMutate") { + ScopedFastFlag luauSelfAssignmentSkip{"LuauSelfAssignmentSkip", true}; + // if the argument is mutated, it gets a register even if the value is constant CHECK_EQ("\n" + compileFunction(R"( local function foo(a) @@ -5231,7 +5233,6 @@ return x 1, 2), R"( DUPCLOSURE R0 K0 -MOVE R0 R0 MOVE R1 R0 LOADN R2 42 CALL R1 1 1 @@ -6790,4 +6791,31 @@ L0: RETURN R1 -1 )"); } +TEST_CASE("SkipSelfAssignment") +{ + ScopedFastFlag luauSelfAssignmentSkip{"LuauSelfAssignmentSkip", true}; + + CHECK_EQ("\n" + compileFunction0("local a a = a"), R"( +LOADNIL R0 +RETURN R0 0 +)"); + + CHECK_EQ("\n" + compileFunction0("local a a = a :: number"), R"( +LOADNIL R0 +RETURN R0 0 +)"); + + CHECK_EQ("\n" + compileFunction0("local a a = (((a)))"), R"( +LOADNIL R0 +RETURN R0 0 +)"); + + // Keep it on optimization level 0 + CHECK_EQ("\n" + compileFunction("local a a = a", 0, 0), R"( +LOADNIL R0 +MOVE R0 R0 +RETURN R0 0 +)"); +} + TEST_SUITE_END(); diff --git a/tests/ConstraintGraphBuilderFixture.cpp b/tests/ConstraintGraphBuilderFixture.cpp index 64e6baaf..4d7ee4fe 100644 --- a/tests/ConstraintGraphBuilderFixture.cpp +++ b/tests/ConstraintGraphBuilderFixture.cpp @@ -1,6 +1,8 @@ // This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details #include "ConstraintGraphBuilderFixture.h" +#include "Luau/TypeReduction.h" + namespace Luau { @@ -9,6 +11,8 @@ ConstraintGraphBuilderFixture::ConstraintGraphBuilderFixture() , mainModule(new Module) , forceTheFlag{"DebugLuauDeferredConstraintResolution", true} { + mainModule->reduction = std::make_unique(NotNull{&mainModule->internalTypes}, builtinTypes, NotNull{&ice}); + BlockedType::nextIndex = 0; BlockedTypePack::nextIndex = 0; } diff --git a/tests/Fixture.cpp b/tests/Fixture.cpp index 41629281..5ff00627 100644 --- a/tests/Fixture.cpp +++ b/tests/Fixture.cpp @@ -312,6 +312,9 @@ std::optional Fixture::getType(const std::string& name) ModulePtr module = getMainModule(); REQUIRE(module); + if (!module->hasModuleScope()) + return std::nullopt; + if (FFlag::DebugLuauDeferredConstraintResolution) return linearSearchForBinding(module->getModuleScope().get(), name.c_str()); else @@ -329,11 +332,14 @@ TypeId Fixture::requireType(const ModuleName& moduleName, const std::string& nam { ModulePtr module = frontend.moduleResolver.getModule(moduleName); REQUIRE(module); - return requireType(module->getModuleScope(), name); + return requireType(module, name); } TypeId Fixture::requireType(const ModulePtr& module, const std::string& name) { + if (!module->hasModuleScope()) + FAIL("requireType: module scope data is not available"); + return requireType(module->getModuleScope(), name); } @@ -367,7 +373,12 @@ TypeId Fixture::requireTypeAtPosition(Position position) std::optional Fixture::lookupType(const std::string& name) { - if (auto typeFun = getMainModule()->getModuleScope()->lookupType(name)) + ModulePtr module = getMainModule(); + + if (!module->hasModuleScope()) + return std::nullopt; + + if (auto typeFun = module->getModuleScope()->lookupType(name)) return typeFun->type; return std::nullopt; @@ -375,12 +386,24 @@ std::optional Fixture::lookupType(const std::string& name) std::optional Fixture::lookupImportedType(const std::string& moduleAlias, const std::string& name) { - if (auto typeFun = getMainModule()->getModuleScope()->lookupImportedType(moduleAlias, name)) + ModulePtr module = getMainModule(); + + if (!module->hasModuleScope()) + FAIL("lookupImportedType: module scope data is not available"); + + if (auto typeFun = module->getModuleScope()->lookupImportedType(moduleAlias, name)) return typeFun->type; return std::nullopt; } +TypeId Fixture::requireTypeAlias(const std::string& name) +{ + std::optional ty = lookupType(name); + REQUIRE(ty); + return *ty; +} + std::string Fixture::decorateWithTypes(const std::string& code) { fileResolver.source[mainModuleName] = code; @@ -552,15 +575,52 @@ std::optional linearSearchForBinding(Scope* scope, const char* name) return std::nullopt; } -void registerHiddenTypes(Fixture& fixture, TypeArena& arena) +void registerHiddenTypes(Frontend* frontend) { - TypeId t = arena.addType(GenericType{"T"}); + TypeId t = frontend->globalTypes.addType(GenericType{"T"}); GenericTypeDefinition genericT{t}; - ScopePtr moduleScope = fixture.frontend.getGlobalScope(); - moduleScope->exportedTypeBindings["Not"] = TypeFun{{genericT}, arena.addType(NegationType{t})}; - moduleScope->exportedTypeBindings["fun"] = TypeFun{{}, fixture.builtinTypes->functionType}; - moduleScope->exportedTypeBindings["cls"] = TypeFun{{}, fixture.builtinTypes->classType}; + ScopePtr globalScope = frontend->getGlobalScope(); + globalScope->exportedTypeBindings["Not"] = TypeFun{{genericT}, frontend->globalTypes.addType(NegationType{t})}; + globalScope->exportedTypeBindings["fun"] = TypeFun{{}, frontend->builtinTypes->functionType}; + globalScope->exportedTypeBindings["cls"] = TypeFun{{}, frontend->builtinTypes->classType}; + globalScope->exportedTypeBindings["err"] = TypeFun{{}, frontend->builtinTypes->errorType}; +} + +void createSomeClasses(Frontend* frontend) +{ + TypeArena& arena = frontend->globalTypes; + unfreeze(arena); + + ScopePtr moduleScope = frontend->getGlobalScope(); + + TypeId parentType = arena.addType(ClassType{"Parent", {}, frontend->builtinTypes->classType, std::nullopt, {}, nullptr, "Test"}); + + ClassType* parentClass = getMutable(parentType); + parentClass->props["method"] = {makeFunction(arena, parentType, {}, {})}; + + parentClass->props["virtual_method"] = {makeFunction(arena, parentType, {}, {})}; + + addGlobalBinding(*frontend, "Parent", {parentType}); + moduleScope->exportedTypeBindings["Parent"] = TypeFun{{}, parentType}; + + TypeId childType = arena.addType(ClassType{"Child", {}, parentType, std::nullopt, {}, nullptr, "Test"}); + + ClassType* childClass = getMutable(childType); + childClass->props["virtual_method"] = {makeFunction(arena, childType, {}, {})}; + + addGlobalBinding(*frontend, "Child", {childType}); + moduleScope->exportedTypeBindings["Child"] = TypeFun{{}, childType}; + + TypeId unrelatedType = arena.addType(ClassType{"Unrelated", {}, frontend->builtinTypes->classType, std::nullopt, {}, nullptr, "Test"}); + + addGlobalBinding(*frontend, "Unrelated", {unrelatedType}); + moduleScope->exportedTypeBindings["Unrelated"] = TypeFun{{}, unrelatedType}; + + for (const auto& [name, ty] : moduleScope->exportedTypeBindings) + persist(ty.type); + + freeze(arena); } void dump(const std::vector& constraints) diff --git a/tests/Fixture.h b/tests/Fixture.h index 3edd6b4c..6dc8abf2 100644 --- a/tests/Fixture.h +++ b/tests/Fixture.h @@ -91,6 +91,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); ScopedFastFlag sff_DebugLuauFreezeArena; ScopedFastFlag sff_UnknownNever{"LuauUnknownAndNeverType", true}; @@ -151,7 +152,8 @@ std::optional lookupName(ScopePtr scope, const std::string& name); // Wa std::optional linearSearchForBinding(Scope* scope, const char* name); -void registerHiddenTypes(Fixture& fixture, TypeArena& arena); +void registerHiddenTypes(Frontend* frontend); +void createSomeClasses(Frontend* frontend); } // namespace Luau diff --git a/tests/Frontend.test.cpp b/tests/Frontend.test.cpp index 93df5605..a69965e0 100644 --- a/tests/Frontend.test.cpp +++ b/tests/Frontend.test.cpp @@ -10,6 +10,8 @@ #include +LUAU_FASTFLAG(LuauScopelessModule) + using namespace Luau; namespace @@ -143,6 +145,8 @@ TEST_CASE_FIXTURE(FrontendFixture, "real_source") TEST_CASE_FIXTURE(FrontendFixture, "automatically_check_dependent_scripts") { + ScopedFastFlag luauScopelessModule{"LuauScopelessModule", true}; + fileResolver.source["game/Gui/Modules/A"] = "return {hello=5, world=true}"; fileResolver.source["game/Gui/Modules/B"] = R"( local Modules = game:GetService('Gui').Modules @@ -157,7 +161,7 @@ TEST_CASE_FIXTURE(FrontendFixture, "automatically_check_dependent_scripts") CHECK(bModule->errors.empty()); Luau::dumpErrors(bModule); - auto bExports = first(bModule->getModuleScope()->returnType); + auto bExports = first(bModule->returnType); REQUIRE(!!bExports); CHECK_EQ("{| b_value: number |}", toString(*bExports)); @@ -220,6 +224,8 @@ TEST_CASE_FIXTURE(FrontendFixture, "any_annotation_breaks_cycle") TEST_CASE_FIXTURE(FrontendFixture, "nocheck_modules_are_typed") { + ScopedFastFlag luauScopelessModule{"LuauScopelessModule", true}; + fileResolver.source["game/Gui/Modules/A"] = R"( --!nocheck export type Foo = number @@ -243,13 +249,13 @@ TEST_CASE_FIXTURE(FrontendFixture, "nocheck_modules_are_typed") ModulePtr aModule = frontend.moduleResolver.modules["game/Gui/Modules/A"]; REQUIRE(bool(aModule)); - std::optional aExports = first(aModule->getModuleScope()->returnType); + std::optional aExports = first(aModule->returnType); REQUIRE(bool(aExports)); ModulePtr bModule = frontend.moduleResolver.modules["game/Gui/Modules/B"]; REQUIRE(bool(bModule)); - std::optional bExports = first(bModule->getModuleScope()->returnType); + std::optional bExports = first(bModule->returnType); REQUIRE(bool(bExports)); CHECK_EQ(toString(*aExports), toString(*bExports)); @@ -275,6 +281,8 @@ TEST_CASE_FIXTURE(FrontendFixture, "cycle_detection_between_check_and_nocheck") TEST_CASE_FIXTURE(FrontendFixture, "nocheck_cycle_used_by_checked") { + ScopedFastFlag luauScopelessModule{"LuauScopelessModule", true}; + fileResolver.source["game/Gui/Modules/A"] = R"( --!nocheck local Modules = game:GetService('Gui').Modules @@ -300,7 +308,7 @@ TEST_CASE_FIXTURE(FrontendFixture, "nocheck_cycle_used_by_checked") ModulePtr cModule = frontend.moduleResolver.modules["game/Gui/Modules/C"]; REQUIRE(bool(cModule)); - std::optional cExports = first(cModule->getModuleScope()->returnType); + std::optional cExports = first(cModule->returnType); REQUIRE(bool(cExports)); CHECK_EQ("{| a: any, b: any |}", toString(*cExports)); } @@ -493,6 +501,8 @@ TEST_CASE_FIXTURE(FrontendFixture, "dont_recheck_script_that_hasnt_been_marked_d TEST_CASE_FIXTURE(FrontendFixture, "recheck_if_dependent_script_is_dirty") { + ScopedFastFlag luauScopelessModule{"LuauScopelessModule", true}; + fileResolver.source["game/Gui/Modules/A"] = "return {hello=5, world=true}"; fileResolver.source["game/Gui/Modules/B"] = R"( local Modules = game:GetService('Gui').Modules @@ -511,7 +521,7 @@ TEST_CASE_FIXTURE(FrontendFixture, "recheck_if_dependent_script_is_dirty") CHECK(bModule->errors.empty()); Luau::dumpErrors(bModule); - auto bExports = first(bModule->getModuleScope()->returnType); + auto bExports = first(bModule->returnType); REQUIRE(!!bExports); CHECK_EQ("{| b_value: string |}", toString(*bExports)); diff --git a/tests/Module.test.cpp b/tests/Module.test.cpp index 5f97fb6c..34c2e8fd 100644 --- a/tests/Module.test.cpp +++ b/tests/Module.test.cpp @@ -112,6 +112,8 @@ TEST_CASE_FIXTURE(Fixture, "deepClone_cyclic_table") TEST_CASE_FIXTURE(BuiltinsFixture, "builtin_types_point_into_globalTypes_arena") { + ScopedFastFlag luauScopelessModule{"LuauScopelessModule", true}; + CheckResult result = check(R"( return {sign=math.sign} )"); @@ -119,7 +121,7 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "builtin_types_point_into_globalTypes_arena") LUAU_REQUIRE_NO_ERRORS(result); ModulePtr module = frontend.moduleResolver.getModule("MainModule"); - std::optional exports = first(module->getModuleScope()->returnType); + std::optional exports = first(module->returnType); REQUIRE(bool(exports)); REQUIRE(isInArena(*exports, module->interfaceTypes)); @@ -283,6 +285,8 @@ TEST_CASE_FIXTURE(Fixture, "clone_recursion_limit") TEST_CASE_FIXTURE(Fixture, "any_persistance_does_not_leak") { + ScopedFastFlag luauScopelessModule{"LuauScopelessModule", true}; + fileResolver.source["Module/A"] = R"( export type A = B type B = A @@ -294,8 +298,8 @@ type B = A LUAU_REQUIRE_ERRORS(result); auto mod = frontend.moduleResolver.getModule("Module/A"); - auto it = mod->getModuleScope()->exportedTypeBindings.find("A"); - REQUIRE(it != mod->getModuleScope()->exportedTypeBindings.end()); + auto it = mod->exportedTypeBindings.find("A"); + REQUIRE(it != mod->exportedTypeBindings.end()); CHECK(toString(it->second.type) == "any"); } @@ -306,6 +310,7 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "do_not_clone_reexports") {"LuauSubstitutionReentrant", true}, {"LuauClassTypeVarsInSubstitution", true}, {"LuauSubstitutionFixMissingFields", true}, + {"LuauScopelessModule", true}, }; fileResolver.source["Module/A"] = R"( @@ -326,10 +331,10 @@ return {} ModulePtr modB = frontend.moduleResolver.getModule("Module/B"); REQUIRE(modA); REQUIRE(modB); - auto modAiter = modA->getModuleScope()->exportedTypeBindings.find("A"); - auto modBiter = modB->getModuleScope()->exportedTypeBindings.find("B"); - REQUIRE(modAiter != modA->getModuleScope()->exportedTypeBindings.end()); - REQUIRE(modBiter != modB->getModuleScope()->exportedTypeBindings.end()); + auto modAiter = modA->exportedTypeBindings.find("A"); + auto modBiter = modB->exportedTypeBindings.find("B"); + REQUIRE(modAiter != modA->exportedTypeBindings.end()); + REQUIRE(modBiter != modB->exportedTypeBindings.end()); TypeId typeA = modAiter->second.type; TypeId typeB = modBiter->second.type; TableType* tableB = getMutable(typeB); @@ -344,6 +349,7 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "do_not_clone_types_of_reexported_values") {"LuauSubstitutionReentrant", true}, {"LuauClassTypeVarsInSubstitution", true}, {"LuauSubstitutionFixMissingFields", true}, + {"LuauScopelessModule", true}, }; fileResolver.source["Module/A"] = R"( @@ -364,8 +370,8 @@ return exports ModulePtr modB = frontend.moduleResolver.getModule("Module/B"); REQUIRE(modA); REQUIRE(modB); - std::optional typeA = first(modA->getModuleScope()->returnType); - std::optional typeB = first(modB->getModuleScope()->returnType); + std::optional typeA = first(modA->returnType); + std::optional typeB = first(modB->returnType); REQUIRE(typeA); REQUIRE(typeB); TableType* tableA = getMutable(*typeA); diff --git a/tests/NonstrictMode.test.cpp b/tests/NonstrictMode.test.cpp index 8a25a5e5..5deeb35d 100644 --- a/tests/NonstrictMode.test.cpp +++ b/tests/NonstrictMode.test.cpp @@ -253,6 +253,8 @@ TEST_CASE_FIXTURE(Fixture, "delay_function_does_not_require_its_argument_to_retu TEST_CASE_FIXTURE(Fixture, "inconsistent_module_return_types_are_ok") { + ScopedFastFlag luauScopelessModule{"LuauScopelessModule", true}; + CheckResult result = check(R"( --!nonstrict @@ -269,7 +271,7 @@ TEST_CASE_FIXTURE(Fixture, "inconsistent_module_return_types_are_ok") LUAU_REQUIRE_NO_ERRORS(result); - REQUIRE_EQ("any", toString(getMainModule()->getModuleScope()->returnType)); + REQUIRE_EQ("any", toString(getMainModule()->returnType)); } TEST_CASE_FIXTURE(Fixture, "returning_insufficient_return_values") diff --git a/tests/Normalize.test.cpp b/tests/Normalize.test.cpp index 2ee82623..615fc997 100644 --- a/tests/Normalize.test.cpp +++ b/tests/Normalize.test.cpp @@ -17,46 +17,17 @@ struct IsSubtypeFixture : Fixture { bool isSubtype(TypeId a, TypeId b) { - return ::Luau::isSubtype(a, b, NotNull{getMainModule()->getModuleScope().get()}, builtinTypes, ice); + ModulePtr module = getMainModule(); + REQUIRE(module); + + if (!module->hasModuleScope()) + FAIL("isSubtype: module scope data is not available"); + + return ::Luau::isSubtype(a, b, NotNull{module->getModuleScope().get()}, builtinTypes, ice); } }; } // namespace -void createSomeClasses(Frontend& frontend) -{ - auto& arena = frontend.globalTypes; - - unfreeze(arena); - - TypeId parentType = arena.addType(ClassType{"Parent", {}, frontend.builtinTypes->classType, std::nullopt, {}, nullptr, "Test"}); - - ClassType* parentClass = getMutable(parentType); - parentClass->props["method"] = {makeFunction(arena, parentType, {}, {})}; - - parentClass->props["virtual_method"] = {makeFunction(arena, parentType, {}, {})}; - - addGlobalBinding(frontend, "Parent", {parentType}); - frontend.getGlobalScope()->exportedTypeBindings["Parent"] = TypeFun{{}, parentType}; - - TypeId childType = arena.addType(ClassType{"Child", {}, parentType, std::nullopt, {}, nullptr, "Test"}); - - ClassType* childClass = getMutable(childType); - childClass->props["virtual_method"] = {makeFunction(arena, childType, {}, {})}; - - addGlobalBinding(frontend, "Child", {childType}); - frontend.getGlobalScope()->exportedTypeBindings["Child"] = TypeFun{{}, childType}; - - TypeId unrelatedType = arena.addType(ClassType{"Unrelated", {}, frontend.builtinTypes->classType, std::nullopt, {}, nullptr, "Test"}); - - addGlobalBinding(frontend, "Unrelated", {unrelatedType}); - frontend.getGlobalScope()->exportedTypeBindings["Unrelated"] = TypeFun{{}, unrelatedType}; - - for (const auto& [name, ty] : frontend.getGlobalScope()->exportedTypeBindings) - persist(ty.type); - - freeze(arena); -} - TEST_SUITE_BEGIN("isSubtype"); TEST_CASE_FIXTURE(IsSubtypeFixture, "primitives") @@ -352,7 +323,7 @@ TEST_CASE_FIXTURE(IsSubtypeFixture, "cyclic_table") TEST_CASE_FIXTURE(IsSubtypeFixture, "classes") { - createSomeClasses(frontend); + createSomeClasses(&frontend); check(""); // Ensure that we have a main Module. @@ -403,7 +374,7 @@ struct NormalizeFixture : Fixture NormalizeFixture() { - registerHiddenTypes(*this, arena); + registerHiddenTypes(&frontend); } const NormalizedType* toNormalizedType(const std::string& annotation) @@ -589,7 +560,7 @@ TEST_CASE_FIXTURE(Fixture, "cyclic_table_normalizes_sensibly") TEST_CASE_FIXTURE(BuiltinsFixture, "skip_force_normal_on_external_types") { - createSomeClasses(frontend); + createSomeClasses(&frontend); CheckResult result = check(R"( export type t0 = { a: Child } @@ -612,7 +583,7 @@ TEST_CASE_FIXTURE(NormalizeFixture, "unions_of_classes") { ScopedFastFlag sff{"LuauNegatedClassTypes", true}; - createSomeClasses(frontend); + createSomeClasses(&frontend); CHECK("Parent | Unrelated" == toString(normal("Parent | Unrelated"))); CHECK("Parent" == toString(normal("Parent | Child"))); CHECK("Parent | Unrelated" == toString(normal("Parent | Child | Unrelated"))); @@ -622,7 +593,7 @@ TEST_CASE_FIXTURE(NormalizeFixture, "intersections_of_classes") { ScopedFastFlag sff{"LuauNegatedClassTypes", true}; - createSomeClasses(frontend); + createSomeClasses(&frontend); CHECK("Child" == toString(normal("Parent & Child"))); CHECK("never" == toString(normal("Child & Unrelated"))); } @@ -631,7 +602,7 @@ TEST_CASE_FIXTURE(NormalizeFixture, "narrow_union_of_classes_with_intersection") { ScopedFastFlag sff{"LuauNegatedClassTypes", true}; - createSomeClasses(frontend); + createSomeClasses(&frontend); CHECK("Child" == toString(normal("(Child | Unrelated) & Child"))); } @@ -639,7 +610,7 @@ TEST_CASE_FIXTURE(NormalizeFixture, "negations_of_classes") { ScopedFastFlag sff{"LuauNegatedClassTypes", true}; - createSomeClasses(frontend); + createSomeClasses(&frontend); CHECK("(Parent & ~Child) | Unrelated" == toString(normal("(Parent & Not) | Unrelated"))); CHECK("((class & ~Child) | boolean | function | number | string | thread)?" == toString(normal("Not"))); CHECK("Child" == toString(normal("Not & Child"))); @@ -653,7 +624,7 @@ TEST_CASE_FIXTURE(NormalizeFixture, "classes_and_unknown") { ScopedFastFlag sff{"LuauNegatedClassTypes", true}; - createSomeClasses(frontend); + createSomeClasses(&frontend); CHECK("Parent" == toString(normal("Parent & unknown"))); } @@ -661,7 +632,7 @@ TEST_CASE_FIXTURE(NormalizeFixture, "classes_and_never") { ScopedFastFlag sff{"LuauNegatedClassTypes", true}; - createSomeClasses(frontend); + createSomeClasses(&frontend); CHECK("never" == toString(normal("Parent & never"))); } diff --git a/tests/TypeInfer.aliases.test.cpp b/tests/TypeInfer.aliases.test.cpp index 4dd82269..9b65cf24 100644 --- a/tests/TypeInfer.aliases.test.cpp +++ b/tests/TypeInfer.aliases.test.cpp @@ -9,7 +9,6 @@ using namespace Luau; LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution) LUAU_FASTFLAG(LuauTypeMismatchInvarianceInError) -LUAU_FASTFLAG(LuauNewLibraryTypeNames) TEST_SUITE_BEGIN("TypeAliases"); @@ -506,19 +505,14 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "general_require_multi_assign") CheckResult result = frontend.check("workspace/C"); LUAU_REQUIRE_NO_ERRORS(result); - ModulePtr m = frontend.moduleResolver.modules["workspace/C"]; - REQUIRE(m != nullptr); - - std::optional aTypeId = lookupName(m->getModuleScope(), "a"); - REQUIRE(aTypeId); - const Luau::TableType* aType = get(follow(*aTypeId)); + TypeId aTypeId = requireType("workspace/C", "a"); + const Luau::TableType* aType = get(follow(aTypeId)); REQUIRE(aType); REQUIRE(aType->props.size() == 2); - std::optional bTypeId = lookupName(m->getModuleScope(), "b"); - REQUIRE(bTypeId); - const Luau::TableType* bType = get(follow(*bTypeId)); + TypeId bTypeId = requireType("workspace/C", "b"); + const Luau::TableType* bType = get(follow(bTypeId)); REQUIRE(bType); REQUIRE(bType->props.size() == 3); } @@ -530,10 +524,7 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "type_alias_import_mutation") TypeId ty = getGlobalBinding(frontend, "table"); - if (FFlag::LuauNewLibraryTypeNames) - CHECK(toString(ty) == "typeof(table)"); - else - CHECK(toString(ty) == "table"); + CHECK(toString(ty) == "typeof(table)"); const TableType* ttv = get(ty); REQUIRE(ttv); diff --git a/tests/TypeInfer.annotations.test.cpp b/tests/TypeInfer.annotations.test.cpp index bf66ecbc..3e98367c 100644 --- a/tests/TypeInfer.annotations.test.cpp +++ b/tests/TypeInfer.annotations.test.cpp @@ -319,10 +319,10 @@ TEST_CASE_FIXTURE(Fixture, "self_referential_type_alias") LUAU_REQUIRE_NO_ERRORS(result); - std::optional res = getMainModule()->getModuleScope()->lookupType("O"); + std::optional res = lookupType("O"); REQUIRE(res); - TypeId oType = follow(res->type); + TypeId oType = follow(*res); const TableType* oTable = get(oType); REQUIRE(oTable); @@ -347,6 +347,8 @@ TEST_CASE_FIXTURE(Fixture, "define_generic_type_alias") LUAU_REQUIRE_NO_ERRORS(result); ModulePtr mainModule = getMainModule(); + REQUIRE(mainModule); + REQUIRE(mainModule->hasModuleScope()); auto it = mainModule->getModuleScope()->privateTypeBindings.find("Array"); REQUIRE(it != mainModule->getModuleScope()->privateTypeBindings.end()); @@ -463,6 +465,8 @@ TEST_CASE_FIXTURE(Fixture, "type_alias_always_resolve_to_a_real_type") TEST_CASE_FIXTURE(Fixture, "interface_types_belong_to_interface_arena") { + ScopedFastFlag luauScopelessModule{"LuauScopelessModule", true}; + CheckResult result = check(R"( export type A = {field: number} @@ -475,12 +479,12 @@ TEST_CASE_FIXTURE(Fixture, "interface_types_belong_to_interface_arena") Module& mod = *getMainModule(); - const TypeFun& a = mod.getModuleScope()->exportedTypeBindings["A"]; + const TypeFun& a = mod.exportedTypeBindings["A"]; CHECK(isInArena(a.type, mod.interfaceTypes)); CHECK(!isInArena(a.type, typeChecker.globalTypes)); - std::optional exportsType = first(mod.getModuleScope()->returnType); + std::optional exportsType = first(mod.returnType); REQUIRE(exportsType); TableType* exportsTable = getMutable(*exportsType); @@ -494,6 +498,8 @@ TEST_CASE_FIXTURE(Fixture, "interface_types_belong_to_interface_arena") TEST_CASE_FIXTURE(Fixture, "generic_aliases_are_cloned_properly") { + ScopedFastFlag luauScopelessModule{"LuauScopelessModule", true}; + CheckResult result = check(R"( export type Array = { [number]: T } )"); @@ -501,7 +507,7 @@ TEST_CASE_FIXTURE(Fixture, "generic_aliases_are_cloned_properly") dumpErrors(result); Module& mod = *getMainModule(); - const auto& typeBindings = mod.getModuleScope()->exportedTypeBindings; + const auto& typeBindings = mod.exportedTypeBindings; auto it = typeBindings.find("Array"); REQUIRE(typeBindings.end() != it); @@ -521,6 +527,8 @@ TEST_CASE_FIXTURE(Fixture, "generic_aliases_are_cloned_properly") TEST_CASE_FIXTURE(Fixture, "cloned_interface_maintains_pointers_between_definitions") { + ScopedFastFlag luauScopelessModule{"LuauScopelessModule", true}; + CheckResult result = check(R"( export type Record = { name: string, location: string } local a: Record = { name="Waldo", location="?????" } @@ -533,9 +541,9 @@ TEST_CASE_FIXTURE(Fixture, "cloned_interface_maintains_pointers_between_definiti Module& mod = *getMainModule(); - TypeId recordType = mod.getModuleScope()->exportedTypeBindings["Record"].type; + TypeId recordType = mod.exportedTypeBindings["Record"].type; - std::optional exportsType = first(mod.getModuleScope()->returnType); + std::optional exportsType = first(mod.returnType); REQUIRE(exportsType); TableType* exportsTable = getMutable(*exportsType); diff --git a/tests/TypeInfer.functions.test.cpp b/tests/TypeInfer.functions.test.cpp index a97cea21..70de13d1 100644 --- a/tests/TypeInfer.functions.test.cpp +++ b/tests/TypeInfer.functions.test.cpp @@ -109,6 +109,8 @@ TEST_CASE_FIXTURE(Fixture, "vararg_functions_should_allow_calls_of_any_types_and TEST_CASE_FIXTURE(BuiltinsFixture, "vararg_function_is_quantified") { + ScopedFastFlag luauScopelessModule{"LuauScopelessModule", true}; + CheckResult result = check(R"( local T = {} function T.f(...) @@ -129,7 +131,7 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "vararg_function_is_quantified") LUAU_REQUIRE_NO_ERRORS(result); - auto r = first(getMainModule()->getModuleScope()->returnType); + auto r = first(getMainModule()->returnType); REQUIRE(r); TableType* ttv = getMutable(*r); @@ -1772,7 +1774,7 @@ z = y -- Not OK, so the line is colorable TEST_CASE_FIXTURE(Fixture, "function_is_supertype_of_concrete_functions") { ScopedFastFlag sff{"LuauNegatedFunctionTypes", true}; - registerHiddenTypes(*this, frontend.globalTypes); + registerHiddenTypes(&frontend); CheckResult result = check(R"( function foo(f: fun) end @@ -1791,7 +1793,7 @@ TEST_CASE_FIXTURE(Fixture, "function_is_supertype_of_concrete_functions") TEST_CASE_FIXTURE(Fixture, "concrete_functions_are_not_supertypes_of_function") { ScopedFastFlag sff{"LuauNegatedFunctionTypes", true}; - registerHiddenTypes(*this, frontend.globalTypes); + registerHiddenTypes(&frontend); CheckResult result = check(R"( local a: fun = function() end @@ -1812,7 +1814,7 @@ TEST_CASE_FIXTURE(Fixture, "concrete_functions_are_not_supertypes_of_function") TEST_CASE_FIXTURE(Fixture, "other_things_are_not_related_to_function") { ScopedFastFlag sff{"LuauNegatedFunctionTypes", true}; - registerHiddenTypes(*this, frontend.globalTypes); + registerHiddenTypes(&frontend); CheckResult result = check(R"( local a: fun = function() end diff --git a/tests/TypeInfer.generics.test.cpp b/tests/TypeInfer.generics.test.cpp index 7b417621..3861a8b6 100644 --- a/tests/TypeInfer.generics.test.cpp +++ b/tests/TypeInfer.generics.test.cpp @@ -1021,9 +1021,9 @@ TEST_CASE_FIXTURE(Fixture, "no_stack_overflow_from_quantifying") LUAU_REQUIRE_ERRORS(result); - std::optional t0 = getMainModule()->getModuleScope()->lookupType("t0"); + std::optional t0 = lookupType("t0"); REQUIRE(t0); - CHECK_EQ("*error-type*", toString(t0->type)); + CHECK_EQ("*error-type*", toString(*t0)); auto it = std::find_if(result.errors.begin(), result.errors.end(), [](TypeError& err) { return get(err); diff --git a/tests/TypeInfer.negations.test.cpp b/tests/TypeInfer.negations.test.cpp index 02350a72..261314a6 100644 --- a/tests/TypeInfer.negations.test.cpp +++ b/tests/TypeInfer.negations.test.cpp @@ -20,7 +20,7 @@ struct NegationFixture : Fixture NegationFixture() { - registerHiddenTypes(*this, arena); + registerHiddenTypes(&frontend); } }; diff --git a/tests/TypeInfer.operators.test.cpp b/tests/TypeInfer.operators.test.cpp index 0196666a..5db5b880 100644 --- a/tests/TypeInfer.operators.test.cpp +++ b/tests/TypeInfer.operators.test.cpp @@ -405,17 +405,41 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "compound_assign_metatable") type V2B = { x: number, y: number } local v2b: V2B = { x = 0, y = 0 } local VMT = {} - type V2 = typeof(setmetatable(v2b, VMT)) - function VMT.__add(a: V2, b: V2): V2 + VMT.__add = function(a: V2, b: V2): V2 return setmetatable({ x = a.x + b.x, y = a.y + b.y }, VMT) end + type V2 = typeof(setmetatable(v2b, VMT)) + local v1: V2 = setmetatable({ x = 1, y = 2 }, VMT) local v2: V2 = setmetatable({ x = 3, y = 4 }, VMT) v1 += v2 )"); - CHECK_EQ(0, result.errors.size()); + + LUAU_REQUIRE_NO_ERRORS(result); +} + +TEST_CASE_FIXTURE(BuiltinsFixture, "compound_assign_result_must_be_compatible_with_var") +{ + CheckResult result = check(R"( + function __add(left, right) + return 123 + end + + local mt = { + __add = __add, + } + + local x = setmetatable({}, mt) + local v: number + + v += x -- okay: number + x -> number + x += v -- not okay: x numberType}}); } TEST_CASE_FIXTURE(BuiltinsFixture, "compound_assign_mismatch_metatable") @@ -1015,11 +1039,11 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "mm_ops_must_return_a_value") local y = x + 123 )"); - LUAU_REQUIRE_ERROR_COUNT(1, result); + LUAU_REQUIRE_ERROR_COUNT(2, result); CHECK(requireType("y") == builtinTypes->errorRecoveryType()); - const GenericError* ge = get(result.errors[0]); + const GenericError* ge = get(result.errors[1]); REQUIRE(ge); CHECK(ge->message == "Metamethod '__add' must return a value"); } @@ -1049,13 +1073,13 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "mm_comparisons_must_return_a_boolean") local v2 = o2 < o2 )"); - LUAU_REQUIRE_ERROR_COUNT(2, result); + LUAU_REQUIRE_ERROR_COUNT(4, result); CHECK(requireType("v1") == builtinTypes->booleanType); CHECK(requireType("v2") == builtinTypes->booleanType); - CHECK(toString(result.errors[0]) == "Metamethod '__lt' must return type 'boolean'"); - CHECK(toString(result.errors[1]) == "Metamethod '__lt' must return type 'boolean'"); + CHECK(toString(result.errors[1]) == "Metamethod '__lt' must return a boolean"); + CHECK(toString(result.errors[3]) == "Metamethod '__lt' must return a boolean"); } TEST_CASE_FIXTURE(BuiltinsFixture, "reworked_and") diff --git a/tests/TypeInfer.provisional.test.cpp b/tests/TypeInfer.provisional.test.cpp index cf969f2d..3e278ca2 100644 --- a/tests/TypeInfer.provisional.test.cpp +++ b/tests/TypeInfer.provisional.test.cpp @@ -516,6 +516,8 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "for_in_loop_with_zero_iterators") // Ideally, we would not try to export a function type with generic types from incorrect scope TEST_CASE_FIXTURE(BuiltinsFixture, "generic_type_leak_to_module_interface") { + ScopedFastFlag luauScopelessModule{"LuauScopelessModule", true}; + fileResolver.source["game/A"] = R"( local wrapStrictTable @@ -548,13 +550,15 @@ return wrapStrictTable(Constants, "Constants") ModulePtr m = frontend.moduleResolver.modules["game/B"]; REQUIRE(m); - std::optional result = first(m->getModuleScope()->returnType); + std::optional result = first(m->returnType); REQUIRE(result); CHECK(get(*result)); } TEST_CASE_FIXTURE(BuiltinsFixture, "generic_type_leak_to_module_interface_variadic") { + ScopedFastFlag luauScopelessModule{"LuauScopelessModule", true}; + fileResolver.source["game/A"] = R"( local wrapStrictTable @@ -587,7 +591,7 @@ return wrapStrictTable(Constants, "Constants") ModulePtr m = frontend.moduleResolver.modules["game/B"]; REQUIRE(m); - std::optional result = first(m->getModuleScope()->returnType); + std::optional result = first(m->returnType); REQUIRE(result); CHECK(get(*result)); } @@ -620,7 +624,13 @@ struct IsSubtypeFixture : Fixture { bool isSubtype(TypeId a, TypeId b) { - return ::Luau::isSubtype(a, b, NotNull{getMainModule()->getModuleScope().get()}, builtinTypes, ice); + ModulePtr module = getMainModule(); + REQUIRE(module); + + if (!module->hasModuleScope()) + FAIL("isSubtype: module scope data is not available"); + + return ::Luau::isSubtype(a, b, NotNull{module->getModuleScope().get()}, builtinTypes, ice); } }; } // namespace diff --git a/tests/TypeInfer.tables.test.cpp b/tests/TypeInfer.tables.test.cpp index e2aa01f9..dc3b7ceb 100644 --- a/tests/TypeInfer.tables.test.cpp +++ b/tests/TypeInfer.tables.test.cpp @@ -18,7 +18,6 @@ LUAU_FASTFLAG(LuauLowerBoundsCalculation); LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution); LUAU_FASTFLAG(LuauInstantiateInSubtyping) LUAU_FASTFLAG(LuauTypeMismatchInvarianceInError) -LUAU_FASTFLAG(LuauNewLibraryTypeNames) TEST_SUITE_BEGIN("TableTests"); @@ -1730,16 +1729,8 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "builtin_table_names") LUAU_REQUIRE_ERROR_COUNT(2, result); - if (FFlag::LuauNewLibraryTypeNames) - { - CHECK_EQ("Cannot add property 'h' to table 'typeof(os)'", toString(result.errors[0])); - CHECK_EQ("Cannot add property 'k' to table 'typeof(string)'", toString(result.errors[1])); - } - else - { - CHECK_EQ("Cannot add property 'h' to table 'os'", toString(result.errors[0])); - CHECK_EQ("Cannot add property 'k' to table 'string'", toString(result.errors[1])); - } + CHECK_EQ("Cannot add property 'h' to table 'typeof(os)'", toString(result.errors[0])); + CHECK_EQ("Cannot add property 'k' to table 'typeof(string)'", toString(result.errors[1])); } TEST_CASE_FIXTURE(BuiltinsFixture, "persistent_sealed_table_is_immutable") @@ -1750,10 +1741,7 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "persistent_sealed_table_is_immutable") )"); LUAU_REQUIRE_ERROR_COUNT(1, result); - if (FFlag::LuauNewLibraryTypeNames) - CHECK_EQ("Cannot add property 'bad' to table 'typeof(os)'", toString(result.errors[0])); - else - CHECK_EQ("Cannot add property 'bad' to table 'os'", toString(result.errors[0])); + CHECK_EQ("Cannot add property 'bad' to table 'typeof(os)'", toString(result.errors[0])); const TableType* osType = get(requireType("os")); REQUIRE(osType != nullptr); @@ -2967,6 +2955,8 @@ TEST_CASE_FIXTURE(Fixture, "inferred_properties_of_a_table_should_start_with_the // The real bug here was that we weren't always uncondionally typechecking a trailing return statement last. TEST_CASE_FIXTURE(BuiltinsFixture, "dont_leak_free_table_props") { + ScopedFastFlag luauScopelessModule{"LuauScopelessModule", true}; + CheckResult result = check(R"( local function a(state) print(state.blah) @@ -2988,7 +2978,7 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "dont_leak_free_table_props") CHECK_EQ("({+ blah: a +}) -> ()", toString(requireType("a"))); CHECK_EQ("({+ gwar: a +}) -> ()", toString(requireType("b"))); - CHECK_EQ("() -> ({+ blah: a, gwar: b +}) -> ()", toString(getMainModule()->getModuleScope()->returnType)); + CHECK_EQ("() -> ({+ blah: a, gwar: b +}) -> ()", toString(getMainModule()->returnType)); } TEST_CASE_FIXTURE(Fixture, "inferred_return_type_of_free_table") @@ -3230,8 +3220,6 @@ TEST_CASE_FIXTURE(Fixture, "scalar_is_a_subtype_of_a_compatible_polymorphic_shap TEST_CASE_FIXTURE(Fixture, "scalar_is_not_a_subtype_of_a_compatible_polymorphic_shape_type") { ScopedFastFlag sff{"LuauScalarShapeSubtyping", true}; - if (!FFlag::LuauNewLibraryTypeNames) - return; CheckResult result = check(R"( local function f(s) @@ -3280,8 +3268,6 @@ TEST_CASE_FIXTURE(Fixture, "a_free_shape_can_turn_into_a_scalar_if_it_is_compati TEST_CASE_FIXTURE(Fixture, "a_free_shape_cannot_turn_into_a_scalar_if_it_is_not_compatible") { ScopedFastFlag sff{"LuauScalarShapeSubtyping", true}; - if (!FFlag::LuauNewLibraryTypeNames) - return; CheckResult result = check(R"( local function f(s): string diff --git a/tests/TypeInfer.test.cpp b/tests/TypeInfer.test.cpp index f6279fa2..f4b84262 100644 --- a/tests/TypeInfer.test.cpp +++ b/tests/TypeInfer.test.cpp @@ -648,10 +648,10 @@ TEST_CASE_FIXTURE(Fixture, "no_stack_overflow_from_isoptional") LUAU_REQUIRE_ERRORS(result); - std::optional t0 = getMainModule()->getModuleScope()->lookupType("t0"); + std::optional t0 = lookupType("t0"); REQUIRE(t0); - CHECK_EQ("*error-type*", toString(t0->type)); + CHECK_EQ("*error-type*", toString(*t0)); auto it = std::find_if(result.errors.begin(), result.errors.end(), [](TypeError& err) { return get(err); diff --git a/tests/TypeInfer.typePacks.cpp b/tests/TypeInfer.typePacks.cpp index b753d30e..94448cfa 100644 --- a/tests/TypeInfer.typePacks.cpp +++ b/tests/TypeInfer.typePacks.cpp @@ -428,8 +428,12 @@ type E = X<(number, ...string)> LUAU_REQUIRE_NO_ERRORS(result); - CHECK_EQ(toString(*lookupType("D")), "(...number) -> (string, ...number)"); - CHECK_EQ(toString(*lookupType("E")), "(number, ...string) -> (string, number, ...string)"); + auto d = lookupType("D"); + REQUIRE(d); + auto e = lookupType("E"); + REQUIRE(e); + CHECK_EQ(toString(*d), "(...number) -> (string, ...number)"); + CHECK_EQ(toString(*e), "(number, ...string) -> (string, number, ...string)"); } TEST_CASE_FIXTURE(Fixture, "type_alias_type_pack_multi") @@ -887,9 +891,13 @@ TEST_CASE_FIXTURE(Fixture, "unifying_vararg_pack_with_fixed_length_pack_produces LUAU_REQUIRE_NO_ERRORS(result); - REQUIRE(bool(getMainModule()->getModuleScope()->varargPack)); + ModulePtr mainModule = getMainModule(); + REQUIRE(mainModule); + REQUIRE(mainModule->hasModuleScope()); - TypePackId varargPack = *getMainModule()->getModuleScope()->varargPack; + REQUIRE(bool(mainModule->getModuleScope()->varargPack)); + + TypePackId varargPack = *mainModule->getModuleScope()->varargPack; auto iter = begin(varargPack); auto endIter = end(varargPack); diff --git a/tests/TypeInfer.unionTypes.test.cpp b/tests/TypeInfer.unionTypes.test.cpp index d3022095..8831bb2e 100644 --- a/tests/TypeInfer.unionTypes.test.cpp +++ b/tests/TypeInfer.unionTypes.test.cpp @@ -397,8 +397,6 @@ local e = a.z TEST_CASE_FIXTURE(Fixture, "optional_iteration") { - ScopedFastFlag luauNilIterator{"LuauNilIterator", true}; - CheckResult result = check(R"( function foo(values: {number}?) local s = 0 diff --git a/tests/TypeReduction.test.cpp b/tests/TypeReduction.test.cpp new file mode 100644 index 00000000..c629b3e3 --- /dev/null +++ b/tests/TypeReduction.test.cpp @@ -0,0 +1,1249 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#include "Luau/TypeReduction.h" + +#include "Fixture.h" +#include "doctest.h" + +using namespace Luau; + +namespace +{ +struct ReductionFixture : Fixture +{ + TypeArena arena; + InternalErrorReporter iceHandler; + UnifierSharedState unifierState{&iceHandler}; + TypeReduction reduction{NotNull{&arena}, builtinTypes, NotNull{&iceHandler}}; + + ReductionFixture() + { + registerHiddenTypes(&frontend); + createSomeClasses(&frontend); + } + + TypeId reductionof(TypeId ty) + { + std::optional reducedTy = reduction.reduce(ty); + REQUIRE(reducedTy); + return *reducedTy; + } + + std::optional tryReduce(const std::string& annotation) + { + CheckResult result = check("type _Res = " + annotation); + LUAU_REQUIRE_NO_ERRORS(result); + return reduction.reduce(requireTypeAlias("_Res")); + } + + TypeId reductionof(const std::string& annotation) + { + std::optional reducedTy = tryReduce(annotation); + REQUIRE_MESSAGE(reducedTy, "Exceeded the cartesian product of the type"); + return *reducedTy; + } +}; +} // namespace + +TEST_SUITE_BEGIN("TypeReductionTests"); + +TEST_CASE_FIXTURE(ReductionFixture, "cartesian_product_exceeded") +{ + ScopedFastInt sfi{"LuauTypeReductionCartesianProductLimit", 5}; + + std::optional ty = tryReduce(R"( + string & (number | string | boolean) & (number | string | boolean) + )"); + + CHECK(!ty); +} + +TEST_CASE_FIXTURE(ReductionFixture, "cartesian_product_exceeded_with_normal_limit") +{ + std::optional ty = tryReduce(R"( + string -- 1 = 1 + & (number | string | boolean) -- 1 * 3 = 3 + & (number | string | boolean) -- 3 * 3 = 9 + & (number | string | boolean) -- 9 * 3 = 27 + & (number | string | boolean) -- 27 * 3 = 81 + & (number | string | boolean) -- 81 * 3 = 243 + & (number | string | boolean) -- 243 * 3 = 729 + & (number | string | boolean) -- 729 * 3 = 2187 + & (number | string | boolean) -- 2187 * 3 = 6561 + & (number | string | boolean) -- 6561 * 3 = 19683 + & (number | string | boolean) -- 19683 * 3 = 59049 + & (number | string) -- 59049 * 2 = 118098 + )"); + + CHECK(!ty); +} + +TEST_CASE_FIXTURE(ReductionFixture, "cartesian_product_is_zero") +{ + ScopedFastInt sfi{"LuauTypeReductionCartesianProductLimit", 5}; + + std::optional ty = tryReduce(R"( + string & (number | string | boolean) & (number | string | boolean) & never + )"); + + CHECK(ty); +} + +TEST_CASE_FIXTURE(ReductionFixture, "intersections_without_negations") +{ + SUBCASE("string_and_string") + { + TypeId ty = reductionof("string & string"); + CHECK("string" == toString(ty)); + } + + SUBCASE("never_and_string") + { + TypeId ty = reductionof("never & string"); + CHECK("never" == toString(ty)); + } + + SUBCASE("string_and_never") + { + TypeId ty = reductionof("string & never"); + CHECK("never" == toString(ty)); + } + + SUBCASE("unknown_and_string") + { + TypeId ty = reductionof("unknown & string"); + CHECK("string" == toString(ty)); + } + + SUBCASE("string_and_unknown") + { + TypeId ty = reductionof("string & unknown"); + CHECK("string" == toString(ty)); + } + + SUBCASE("any_and_string") + { + TypeId ty = reductionof("any & string"); + CHECK("string" == toString(ty)); + } + + SUBCASE("string_and_any") + { + TypeId ty = reductionof("string & any"); + CHECK("string" == toString(ty)); + } + + SUBCASE("string_or_number_and_string") + { + TypeId ty = reductionof("(string | number) & string"); + CHECK("string" == toString(ty)); + } + + SUBCASE("string_and_string_or_number") + { + TypeId ty = reductionof("string & (string | number)"); + CHECK("string" == toString(ty)); + } + + SUBCASE("string_and_a") + { + TypeId ty = reductionof(R"(string & "a")"); + CHECK(R"("a")" == toString(ty)); + } + + SUBCASE("boolean_and_true") + { + TypeId ty = reductionof("boolean & true"); + CHECK("true" == toString(ty)); + } + + SUBCASE("boolean_and_a") + { + TypeId ty = reductionof(R"(boolean & "a")"); + CHECK("never" == toString(ty)); + } + + SUBCASE("a_and_a") + { + TypeId ty = reductionof(R"("a" & "a")"); + CHECK(R"("a")" == toString(ty)); + } + + SUBCASE("a_and_b") + { + TypeId ty = reductionof(R"("a" & "b")"); + CHECK("never" == toString(ty)); + } + + SUBCASE("a_and_true") + { + TypeId ty = reductionof(R"("a" & true)"); + CHECK("never" == toString(ty)); + } + + SUBCASE("a_and_true") + { + TypeId ty = reductionof(R"(true & false)"); + CHECK("never" == toString(ty)); + } + + SUBCASE("function_type_and_function") + { + TypeId ty = reductionof("() -> () & fun"); + CHECK("() -> ()" == toString(ty)); + } + + SUBCASE("function_type_and_string") + { + TypeId ty = reductionof("() -> () & string"); + CHECK("never" == toString(ty)); + } + + SUBCASE("parent_and_child") + { + TypeId ty = reductionof("Parent & Child"); + CHECK("Child" == toString(ty)); + } + + SUBCASE("child_and_parent") + { + TypeId ty = reductionof("Child & Parent"); + CHECK("Child" == toString(ty)); + } + + SUBCASE("child_and_unrelated") + { + TypeId ty = reductionof("Child & Unrelated"); + CHECK("never" == toString(ty)); + } + + SUBCASE("string_and_table") + { + TypeId ty = reductionof("string & {}"); + CHECK("never" == toString(ty)); + } + + SUBCASE("string_and_child") + { + TypeId ty = reductionof("string & Child"); + CHECK("never" == toString(ty)); + } + + SUBCASE("string_and_function") + { + TypeId ty = reductionof("string & () -> ()"); + CHECK("never" == toString(ty)); + } + + SUBCASE("function_and_table") + { + TypeId ty = reductionof("() -> () & {}"); + CHECK("never" == toString(ty)); + } + + SUBCASE("function_and_class") + { + TypeId ty = reductionof("() -> () & Child"); + CHECK("never" == toString(ty)); + } + + SUBCASE("function_and_function") + { + TypeId ty = reductionof("() -> () & () -> ()"); + CHECK("(() -> ()) & (() -> ())" == toString(ty)); + } + + SUBCASE("table_and_table") + { + TypeId ty = reductionof("{} & {}"); + CHECK("{| |}" == toString(ty)); + } + + SUBCASE("table_and_metatable") + { + // No setmetatable in ReductionFixture, so we mix and match. + BuiltinsFixture fixture; + fixture.check(R"( + type Ty = {} & typeof(setmetatable({}, {})) + )"); + + TypeId ty = reductionof(fixture.requireTypeAlias("Ty")); + CHECK("{ @metatable { }, { } } & {| |}" == toString(ty)); + } + + SUBCASE("a_and_string") + { + TypeId ty = reductionof(R"("a" & string)"); + CHECK(R"("a")" == toString(ty)); + } + + SUBCASE("reducible_function_and_function") + { + TypeId ty = reductionof("((string | string) -> (number | number)) & fun"); + CHECK("(string) -> number" == toString(ty)); + } + + SUBCASE("string_and_error") + { + TypeId ty = reductionof("string & err"); + CHECK("*error-type* & string" == toString(ty)); + } + + SUBCASE("table_p_string_and_table_p_number") + { + TypeId ty = reductionof("{ p: string } & { p: number }"); + CHECK("never" == toString(ty)); + } + + SUBCASE("table_p_string_and_table_p_string") + { + TypeId ty = reductionof("{ p: string } & { p: string }"); + CHECK("{| p: string |}" == toString(ty)); + } + + SUBCASE("table_x_table_p_string_and_table_x_table_p_number") + { + TypeId ty = reductionof("{ x: { p: string } } & { x: { p: number } }"); + CHECK("never" == toString(ty)); + } + + SUBCASE("table_p_and_table_q") + { + TypeId ty = reductionof("{ p: string } & { q: number }"); + CHECK("{| p: string, q: number |}" == toString(ty)); + } + + SUBCASE("table_tag_a_or_table_tag_b_and_table_b") + { + TypeId ty = reductionof("({ tag: string, a: number } | { tag: number, b: string }) & { b: string }"); + CHECK("{| a: number, b: string, tag: string |} | {| b: string, tag: number |}" == toString(ty)); + } + + SUBCASE("table_string_number_indexer_and_table_string_number_indexer") + { + TypeId ty = reductionof("{ [string]: number } & { [string]: number }"); + CHECK("{| [string]: number |}" == toString(ty)); + } + + SUBCASE("table_string_number_indexer_and_empty_table") + { + TypeId ty = reductionof("{ [string]: number } & {}"); + CHECK("{| [string]: number |}" == toString(ty)); + } + + SUBCASE("empty_table_table_string_number_indexer") + { + TypeId ty = reductionof("{} & { [string]: number }"); + CHECK("{| [string]: number |}" == toString(ty)); + } + + SUBCASE("string_number_indexer_and_number_number_indexer") + { + TypeId ty = reductionof("{ [string]: number } & { [number]: number }"); + CHECK("never" == toString(ty)); + } + + SUBCASE("table_p_string_and_indexer_number_number") + { + TypeId ty = reductionof("{ p: string } & { [number]: number }"); + CHECK("{| [number]: number, p: string |}" == toString(ty)); + } + + SUBCASE("table_p_string_and_indexer_string_number") + { + TypeId ty = reductionof("{ p: string } & { [string]: number }"); + CHECK("{| [string]: number, p: string |}" == toString(ty)); + } + + SUBCASE("table_p_string_and_table_p_string_plus_indexer_string_number") + { + TypeId ty = reductionof("{ p: string } & { p: string, [string]: number }"); + CHECK("{| [string]: number, p: string |}" == toString(ty)); + } +} // intersections_without_negations + +TEST_CASE_FIXTURE(ReductionFixture, "intersections_with_negations") +{ + SUBCASE("nil_and_not_nil") + { + TypeId ty = reductionof("nil & Not"); + CHECK("never" == toString(ty)); + } + + SUBCASE("nil_and_not_false") + { + TypeId ty = reductionof("nil & Not"); + CHECK("nil" == toString(ty)); + } + + SUBCASE("string_or_nil_and_not_nil") + { + TypeId ty = reductionof("(string?) & Not"); + CHECK("string" == toString(ty)); + } + + SUBCASE("string_or_nil_and_not_false_or_nil") + { + TypeId ty = reductionof("(string?) & Not"); + CHECK("string" == toString(ty)); + } + + SUBCASE("string_or_nil_and_not_false_and_not_nil") + { + TypeId ty = reductionof("(string?) & Not & Not"); + CHECK("string" == toString(ty)); + } + + SUBCASE("not_false_and_bool") + { + TypeId ty = reductionof("Not & boolean"); + CHECK("true" == toString(ty)); + } + + SUBCASE("function_type_and_not_function") + { + TypeId ty = reductionof("() -> () & Not"); + CHECK("never" == toString(ty)); + } + + SUBCASE("function_type_and_not_string") + { + TypeId ty = reductionof("() -> () & Not"); + CHECK("() -> ()" == toString(ty)); + } + + SUBCASE("not_a_and_string_or_nil") + { + TypeId ty = reductionof(R"(Not<"a"> & (string | nil))"); + CHECK(R"((string & ~"a")?)" == toString(ty)); + } + + SUBCASE("not_a_and_a") + { + TypeId ty = reductionof(R"(Not<"a"> & "a")"); + CHECK("never" == toString(ty)); + } + + SUBCASE("not_a_and_b") + { + TypeId ty = reductionof(R"(Not<"a"> & "b")"); + CHECK(R"("b")" == toString(ty)); + } + + SUBCASE("not_string_and_a") + { + TypeId ty = reductionof(R"(Not & "a")"); + CHECK("never" == toString(ty)); + } + + SUBCASE("not_bool_and_true") + { + TypeId ty = reductionof("Not & true"); + CHECK("never" == toString(ty)); + } + + SUBCASE("not_string_and_true") + { + TypeId ty = reductionof("Not & true"); + CHECK("true" == toString(ty)); + } + + SUBCASE("parent_and_not_child") + { + TypeId ty = reductionof("Parent & Not"); + CHECK("Parent & ~Child" == toString(ty)); + } + + SUBCASE("not_child_and_parent") + { + TypeId ty = reductionof("Not & Parent"); + CHECK("Parent & ~Child" == toString(ty)); + } + + SUBCASE("child_and_not_parent") + { + TypeId ty = reductionof("Child & Not"); + CHECK("never" == toString(ty)); + } + + SUBCASE("not_parent_and_child") + { + TypeId ty = reductionof("Not & Child"); + CHECK("never" == toString(ty)); + } + + SUBCASE("not_parent_and_unrelated") + { + TypeId ty = reductionof("Not & Unrelated"); + CHECK("Unrelated" == toString(ty)); + } + + SUBCASE("unrelated_and_not_parent") + { + TypeId ty = reductionof("Unrelated & Not"); + CHECK("Unrelated" == toString(ty)); + } + + SUBCASE("not_unrelated_and_parent") + { + TypeId ty = reductionof("Not & Parent"); + CHECK("Parent" == toString(ty)); + } + + SUBCASE("parent_and_not_unrelated") + { + TypeId ty = reductionof("Parent & Not"); + CHECK("Parent" == toString(ty)); + } + + SUBCASE("reducible_function_and_not_function") + { + TypeId ty = reductionof("((string | string) -> (number | number)) & Not"); + CHECK("never" == toString(ty)); + } + + SUBCASE("string_and_not_error") + { + TypeId ty = reductionof("string & Not"); + CHECK("string & ~*error-type*" == toString(ty)); + } + + SUBCASE("table_p_string_and_table_p_not_number") + { + TypeId ty = reductionof("{ p: string } & { p: Not }"); + CHECK("{| p: string |}" == toString(ty)); + } + + SUBCASE("table_p_string_and_table_p_not_string") + { + TypeId ty = reductionof("{ p: string } & { p: Not }"); + CHECK("never" == toString(ty)); + } + + SUBCASE("table_x_table_p_string_and_table_x_table_p_not_number") + { + TypeId ty = reductionof("{ x: { p: string } } & { x: { p: Not } }"); + CHECK("{| x: {| p: string |} |}" == toString(ty)); + } +} // intersections_with_negations + +TEST_CASE_FIXTURE(ReductionFixture, "unions_without_negations") +{ + SUBCASE("never_or_string") + { + TypeId ty = reductionof("never | string"); + CHECK("string" == toString(ty)); + } + + SUBCASE("string_or_never") + { + TypeId ty = reductionof("string | never"); + CHECK("string" == toString(ty)); + } + + SUBCASE("unknown_or_string") + { + TypeId ty = reductionof("unknown | string"); + CHECK("unknown" == toString(ty)); + } + + SUBCASE("string_or_unknown") + { + TypeId ty = reductionof("string | unknown"); + CHECK("unknown" == toString(ty)); + } + + SUBCASE("any_or_string") + { + TypeId ty = reductionof("any | string"); + CHECK("any" == toString(ty)); + } + + SUBCASE("string_or_any") + { + TypeId ty = reductionof("string | any"); + CHECK("any" == toString(ty)); + } + + SUBCASE("string_or_string_and_number") + { + TypeId ty = reductionof("string | (string & number)"); + CHECK("string" == toString(ty)); + } + + SUBCASE("string_or_string") + { + TypeId ty = reductionof("string | string"); + CHECK("string" == toString(ty)); + } + + SUBCASE("string_or_number") + { + TypeId ty = reductionof("string | number"); + CHECK("number | string" == toString(ty)); + } + + SUBCASE("number_or_string") + { + TypeId ty = reductionof("number | string"); + CHECK("number | string" == toString(ty)); + } + + SUBCASE("string_or_number_or_string") + { + TypeId ty = reductionof("(string | number) | string"); + CHECK("number | string" == toString(ty)); + } + + SUBCASE("string_or_number_or_string_2") + { + TypeId ty = reductionof("string | (number | string)"); + CHECK("number | string" == toString(ty)); + } + + SUBCASE("string_or_string_or_number") + { + TypeId ty = reductionof("string | (string | number)"); + CHECK("number | string" == toString(ty)); + } + + SUBCASE("string_or_string_or_number_or_boolean") + { + TypeId ty = reductionof("string | (string | number | boolean)"); + CHECK("boolean | number | string" == toString(ty)); + } + + SUBCASE("string_or_string_or_boolean_or_number") + { + TypeId ty = reductionof("string | (string | boolean | number)"); + CHECK("boolean | number | string" == toString(ty)); + } + + SUBCASE("string_or_boolean_or_string_or_number") + { + TypeId ty = reductionof("string | (boolean | string | number)"); + CHECK("boolean | number | string" == toString(ty)); + } + + SUBCASE("boolean_or_string_or_number_or_string") + { + TypeId ty = reductionof("(boolean | string | number) | string"); + CHECK("boolean | number | string" == toString(ty)); + } + + SUBCASE("boolean_or_true") + { + TypeId ty = reductionof("boolean | true"); + CHECK("boolean" == toString(ty)); + } + + SUBCASE("boolean_or_false") + { + TypeId ty = reductionof("boolean | false"); + CHECK("boolean" == toString(ty)); + } + + SUBCASE("boolean_or_true_or_false") + { + TypeId ty = reductionof("boolean | true | false"); + CHECK("boolean" == toString(ty)); + } + + SUBCASE("string_or_a") + { + TypeId ty = reductionof(R"(string | "a")"); + CHECK("string" == toString(ty)); + } + + SUBCASE("a_or_a") + { + TypeId ty = reductionof(R"("a" | "a")"); + CHECK(R"("a")" == toString(ty)); + } + + SUBCASE("a_or_b") + { + TypeId ty = reductionof(R"("a" | "b")"); + CHECK(R"("a" | "b")" == toString(ty)); + } + + SUBCASE("a_or_b_or_string") + { + TypeId ty = reductionof(R"("a" | "b" | string)"); + CHECK("string" == toString(ty)); + } + + SUBCASE("unknown_or_any") + { + TypeId ty = reductionof("unknown | any"); + CHECK("unknown" == toString(ty)); + } + + SUBCASE("any_or_unknown") + { + TypeId ty = reductionof("any | unknown"); + CHECK("unknown" == toString(ty)); + } + + SUBCASE("function_type_or_function") + { + TypeId ty = reductionof("() -> () | fun"); + CHECK("function" == toString(ty)); + } + + SUBCASE("function_or_string") + { + TypeId ty = reductionof("fun | string"); + CHECK("function | string" == toString(ty)); + } + + SUBCASE("parent_or_child") + { + TypeId ty = reductionof("Parent | Child"); + CHECK("Parent" == toString(ty)); + } + + SUBCASE("child_or_parent") + { + TypeId ty = reductionof("Child | Parent"); + CHECK("Parent" == toString(ty)); + } + + SUBCASE("parent_or_unrelated") + { + TypeId ty = reductionof("Parent | Unrelated"); + CHECK("Parent | Unrelated" == toString(ty)); + } + + SUBCASE("parent_or_child_or_unrelated") + { + TypeId ty = reductionof("Parent | Child | Unrelated"); + CHECK("Parent | Unrelated" == toString(ty)); + } + + SUBCASE("parent_or_unrelated_or_child") + { + TypeId ty = reductionof("Parent | Unrelated | Child"); + CHECK("Parent | Unrelated" == toString(ty)); + } + + SUBCASE("parent_or_child_or_unrelated_or_child") + { + TypeId ty = reductionof("Parent | Child | Unrelated | Child"); + CHECK("Parent | Unrelated" == toString(ty)); + } + + SUBCASE("string_or_true") + { + TypeId ty = reductionof("string | true"); + CHECK("string | true" == toString(ty)); + } + + SUBCASE("string_or_function") + { + TypeId ty = reductionof("string | () -> ()"); + CHECK("(() -> ()) | string" == toString(ty)); + } + + SUBCASE("string_or_err") + { + TypeId ty = reductionof("string | err"); + CHECK("*error-type* | string" == toString(ty)); + } +} // unions_without_negations + +TEST_CASE_FIXTURE(ReductionFixture, "unions_with_negations") +{ + SUBCASE("string_or_not_string") + { + TypeId ty = reductionof("string | Not"); + CHECK("unknown" == toString(ty)); + } + + SUBCASE("not_string_or_string") + { + TypeId ty = reductionof("Not | string"); + CHECK("unknown" == toString(ty)); + } + + SUBCASE("not_number_or_string") + { + TypeId ty = reductionof("Not | string"); + CHECK("~number" == toString(ty)); + } + + SUBCASE("string_or_not_number") + { + TypeId ty = reductionof("string | Not"); + CHECK("~number" == toString(ty)); + } + + SUBCASE("not_hi_or_string_and_not_hi") + { + TypeId ty = reductionof(R"(Not<"hi"> | (string & Not<"hi">))"); + CHECK(R"(~"hi")" == toString(ty)); + } + + SUBCASE("string_and_not_hi_or_not_hi") + { + TypeId ty = reductionof(R"((string & Not<"hi">) | Not<"hi">)"); + CHECK(R"(~"hi")" == toString(ty)); + } + + SUBCASE("string_or_not_never") + { + TypeId ty = reductionof("string | Not"); + CHECK("unknown" == toString(ty)); + } + + SUBCASE("not_a_or_not_a") + { + TypeId ty = reductionof(R"(Not<"a"> | Not<"a">)"); + CHECK(R"(~"a")" == toString(ty)); + } + + SUBCASE("not_a_or_a") + { + TypeId ty = reductionof(R"(Not<"a"> | "a")"); + CHECK("unknown" == toString(ty)); + } + + SUBCASE("a_or_not_a") + { + TypeId ty = reductionof(R"("a" | Not<"a">)"); + CHECK("unknown" == toString(ty)); + } + + SUBCASE("not_a_or_string") + { + TypeId ty = reductionof(R"(Not<"a"> | string)"); + CHECK("unknown" == toString(ty)); + } + + SUBCASE("string_or_not_a") + { + TypeId ty = reductionof(R"(string | Not<"a">)"); + CHECK("unknown" == toString(ty)); + } + + SUBCASE("not_string_or_a") + { + TypeId ty = reductionof(R"(Not | "a")"); + CHECK(R"("a" | ~string)" == toString(ty)); + } + + SUBCASE("a_or_not_string") + { + TypeId ty = reductionof(R"("a" | Not)"); + CHECK(R"("a" | ~string)" == toString(ty)); + } + + SUBCASE("not_number_or_a") + { + TypeId ty = reductionof(R"(Not | "a")"); + CHECK("~number" == toString(ty)); + } + + SUBCASE("a_or_not_number") + { + TypeId ty = reductionof(R"("a" | Not)"); + CHECK("~number" == toString(ty)); + } + + SUBCASE("not_a_or_not_b") + { + TypeId ty = reductionof(R"(Not<"a"> | Not<"b">)"); + CHECK("unknown" == toString(ty)); + } + + SUBCASE("boolean_or_not_false") + { + TypeId ty = reductionof("boolean | Not"); + CHECK("unknown" == toString(ty)); + } + + SUBCASE("boolean_or_not_true") + { + TypeId ty = reductionof("boolean | Not"); + CHECK("unknown" == toString(ty)); + } + + SUBCASE("false_or_not_false") + { + TypeId ty = reductionof("false | Not"); + CHECK("unknown" == toString(ty)); + } + + SUBCASE("true_or_not_false") + { + TypeId ty = reductionof("true | Not"); + CHECK("~false" == toString(ty)); + } + + SUBCASE("not_boolean_or_true") + { + TypeId ty = reductionof("Not | true"); + CHECK("~false" == toString(ty)); + } + + SUBCASE("not_false_or_not_boolean") + { + TypeId ty = reductionof("Not | Not"); + CHECK("~false" == toString(ty)); + } + + SUBCASE("function_type_or_not_function") + { + TypeId ty = reductionof("() -> () | Not"); + CHECK("(() -> ()) | ~function" == toString(ty)); + } + + SUBCASE("not_parent_or_child") + { + TypeId ty = reductionof("Not | Child"); + CHECK("Child | ~Parent" == toString(ty)); + } + + SUBCASE("child_or_not_parent") + { + TypeId ty = reductionof("Child | Not"); + CHECK("Child | ~Parent" == toString(ty)); + } + + SUBCASE("parent_or_not_child") + { + TypeId ty = reductionof("Parent | Not"); + CHECK("unknown" == toString(ty)); + } + + SUBCASE("not_child_or_parent") + { + TypeId ty = reductionof("Not | Parent"); + CHECK("unknown" == toString(ty)); + } + + SUBCASE("parent_or_not_unrelated") + { + TypeId ty = reductionof("Parent | Not"); + CHECK("~Unrelated" == toString(ty)); + } + + SUBCASE("not_string_or_string_and_not_a") + { + TypeId ty = reductionof(R"(Not | (string & Not<"a">))"); + CHECK(R"(~"a")" == toString(ty)); + } + + SUBCASE("not_string_or_not_string") + { + TypeId ty = reductionof("Not | Not"); + CHECK("~string" == toString(ty)); + } + + SUBCASE("not_string_or_not_number") + { + TypeId ty = reductionof("Not | Not"); + CHECK("unknown" == toString(ty)); + } + + SUBCASE("not_a_or_not_boolean") + { + TypeId ty = reductionof(R"(Not<"a"> | Not)"); + CHECK("unknown" == toString(ty)); + } + + SUBCASE("not_a_or_boolean") + { + TypeId ty = reductionof(R"(Not<"a"> | boolean)"); + CHECK(R"(~"a")" == toString(ty)); + } + + SUBCASE("string_or_err") + { + TypeId ty = reductionof("string | Not"); + CHECK("string | ~*error-type*" == toString(ty)); + } +} // unions_with_negations + +TEST_CASE_FIXTURE(ReductionFixture, "tables") +{ + SUBCASE("reduce_props") + { + ToStringOptions opts; + opts.exhaustive = true; + + TypeId ty = reductionof("{ x: string | string, y: number | number }"); + CHECK("{| x: string, y: number |}" == toString(ty, opts)); + } + + SUBCASE("reduce_indexers") + { + ToStringOptions opts; + opts.exhaustive = true; + + TypeId ty = reductionof("{ [string | string]: number | number }"); + CHECK("{| [string]: number |}" == toString(ty, opts)); + } + + SUBCASE("reduce_instantiated_type_parameters") + { + check(R"( + type Foo = { x: T } + local foo: Foo = { x = "hello" } + )"); + + TypeId ty = reductionof(requireType("foo")); + CHECK("Foo" == toString(ty)); + } + + SUBCASE("reduce_instantiated_type_pack_parameters") + { + check(R"( + type Foo = { x: () -> T... } + local foo: Foo = { x = function() return "hi", 5 end } + )"); + + TypeId ty = reductionof(requireType("foo")); + CHECK("Foo" == toString(ty)); + } + + SUBCASE("reduce_tables_within_tables") + { + ToStringOptions opts; + opts.exhaustive = true; + + TypeId ty = reductionof("{ x: { y: string & number } }"); + CHECK("{| x: {| y: never |} |}" == toString(ty, opts)); + } +} + +TEST_CASE_FIXTURE(ReductionFixture, "metatables") +{ + SUBCASE("reduce_table_part") + { + TableType table; + table.props["x"] = {arena.addType(UnionType{{builtinTypes->stringType, builtinTypes->stringType}})}; + TypeId tableTy = arena.addType(std::move(table)); + + TypeId ty = reductionof(arena.addType(MetatableType{tableTy, arena.addType(TableType{})})); + CHECK("{ @metatable { }, { x: string } }" == toString(ty)); + } + + SUBCASE("reduce_metatable_part") + { + TableType table; + table.props["x"] = {arena.addType(UnionType{{builtinTypes->stringType, builtinTypes->stringType}})}; + TypeId tableTy = arena.addType(std::move(table)); + + TypeId ty = reductionof(arena.addType(MetatableType{arena.addType(TableType{}), tableTy})); + CHECK("{ @metatable { x: string }, { } }" == toString(ty)); + } +} + +TEST_CASE_FIXTURE(ReductionFixture, "functions") +{ + SUBCASE("reduce_parameters") + { + TypeId ty = reductionof("(string | string) -> ()"); + CHECK("(string) -> ()" == toString(ty)); + } + + SUBCASE("reduce_returns") + { + TypeId ty = reductionof("() -> (string | string)"); + CHECK("() -> string" == toString(ty)); + } + + SUBCASE("reduce_parameters_and_returns") + { + TypeId ty = reductionof("(string | string) -> (number | number)"); + CHECK("(string) -> number" == toString(ty)); + } + + SUBCASE("reduce_tail") + { + TypeId ty = reductionof("() -> ...(string | string)"); + CHECK("() -> (...string)" == toString(ty)); + } + + SUBCASE("reduce_head_and_tail") + { + TypeId ty = reductionof("() -> (string | string, number | number, ...(boolean | boolean))"); + CHECK("() -> (string, number, ...boolean)" == toString(ty)); + } + + SUBCASE("reduce_overloaded_functions") + { + TypeId ty = reductionof("((number | number) -> ()) & ((string | string) -> ())"); + CHECK("((number) -> ()) & ((string) -> ())" == toString(ty)); + } +} // functions + +TEST_CASE_FIXTURE(ReductionFixture, "negations") +{ + SUBCASE("not_unknown") + { + TypeId ty = reductionof("Not"); + CHECK("never" == toString(ty)); + } + + SUBCASE("not_never") + { + TypeId ty = reductionof("Not"); + CHECK("unknown" == toString(ty)); + } + + SUBCASE("not_any") + { + TypeId ty = reductionof("Not"); + CHECK("any" == toString(ty)); + } + + SUBCASE("not_not_reduction") + { + TypeId ty = reductionof("Not>"); + CHECK("never" == toString(ty)); + } + + SUBCASE("not_string") + { + TypeId ty = reductionof("Not"); + CHECK("~string" == toString(ty)); + } + + SUBCASE("not_string_or_number") + { + TypeId ty = reductionof("Not"); + CHECK("~number & ~string" == toString(ty)); + } + + SUBCASE("not_string_and_number") + { + TypeId ty = reductionof("Not"); + CHECK("unknown" == toString(ty)); + } + + SUBCASE("not_error") + { + TypeId ty = reductionof("Not"); + CHECK("~*error-type*" == toString(ty)); + } +} // negations + +TEST_CASE_FIXTURE(ReductionFixture, "discriminable_unions") +{ + SUBCASE("cat_or_dog_and_dog") + { + TypeId ty = reductionof(R"(({ tag: "cat", catfood: string } | { tag: "dog", dogfood: string }) & { tag: "dog" })"); + CHECK(R"({| dogfood: string, tag: "dog" |})" == toString(ty)); + } + + SUBCASE("cat_or_dog_and_not_dog") + { + TypeId ty = reductionof(R"(({ tag: "cat", catfood: string } | { tag: "dog", dogfood: string }) & { tag: Not<"dog"> })"); + CHECK(R"({| catfood: string, tag: "cat" |})" == toString(ty)); + } + + SUBCASE("string_or_number_and_number") + { + TypeId ty = reductionof("({ tag: string, a: number } | { tag: number, b: string }) & { tag: string }"); + CHECK("{| a: number, tag: string |}" == toString(ty)); + } + + SUBCASE("string_or_number_and_number") + { + TypeId ty = reductionof("({ tag: string, a: number } | { tag: number, b: string }) & { tag: number }"); + CHECK("{| b: string, tag: number |}" == toString(ty)); + } + + SUBCASE("child_or_unrelated_and_parent") + { + TypeId ty = reductionof("({ tag: Child, x: number } | { tag: Unrelated, y: string }) & { tag: Parent }"); + CHECK("{| tag: Child, x: number |}" == toString(ty)); + } + + SUBCASE("child_or_unrelated_and_not_parent") + { + TypeId ty = reductionof("({ tag: Child, x: number } | { tag: Unrelated, y: string }) & { tag: Not }"); + CHECK("{| tag: Unrelated, y: string |}" == toString(ty)); + } +} + +TEST_CASE_FIXTURE(ReductionFixture, "cycles") +{ + SUBCASE("recursively_defined_function") + { + check("type F = (f: F) -> ()"); + + TypeId ty = reductionof(requireTypeAlias("F")); + CHECK("(t1) -> () where t1 = (t1) -> ()" == toString(ty)); + } + + SUBCASE("recursively_defined_function_and_function") + { + check("type F = (f: F & fun) -> ()"); + + TypeId ty = reductionof(requireTypeAlias("F")); + CHECK("(t1) -> () where t1 = (function & t1) -> ()" == toString(ty)); + } + + SUBCASE("recursively_defined_table") + { + ToStringOptions opts; + opts.exhaustive = true; + + check("type T = { x: T }"); + + TypeId ty = reductionof(requireTypeAlias("T")); + CHECK("{| x: t1 |} where t1 = {| x: t1 |}" == toString(ty, opts)); + } + + SUBCASE("recursively_defined_table_and_table") + { + ToStringOptions opts; + opts.exhaustive = true; + + check("type T = { x: T & {} }"); + + TypeId ty = reductionof(requireTypeAlias("T")); + CHECK("{| x: t1 & {| |} |} where t1 = {| x: t1 & {| |} |}" == toString(ty, opts)); + } + + SUBCASE("recursively_defined_table_and_table_2") + { + ToStringOptions opts; + opts.exhaustive = true; + + check("type T = { x: T } & { x: number }"); + + TypeId ty = reductionof(requireTypeAlias("T")); + CHECK("never" == toString(ty)); + } + + SUBCASE("recursively_defined_table_and_table_3") + { + ToStringOptions opts; + opts.exhaustive = true; + + check("type T = { x: T } & { x: T }"); + + TypeId ty = reductionof(requireTypeAlias("T")); + CHECK("{| x: {| x: t1 |} & {| x: t1 |} & {| x: t2 & t2 & {| x: t1 |} & {| x: t1 |} |} |} where t1 = t2 & {| x: t1 |} ; t2 = {| x: t1 |}" == + toString(ty)); + } +} + +TEST_CASE_FIXTURE(ReductionFixture, "stress_test_recursion_limits") +{ + TypeId ty = arena.addType(IntersectionType{{builtinTypes->numberType, builtinTypes->stringType}}); + for (size_t i = 0; i < 20'000; ++i) + { + TableType table; + table.state = TableState::Sealed; + table.props["x"] = {ty}; + ty = arena.addType(IntersectionType{{arena.addType(table), arena.addType(table)}}); + } + + CHECK(!reduction.reduce(ty)); +} + +TEST_SUITE_END(); diff --git a/tests/TypeVar.test.cpp b/tests/TypeVar.test.cpp index ec0a2473..36e437e2 100644 --- a/tests/TypeVar.test.cpp +++ b/tests/TypeVar.test.cpp @@ -1,7 +1,8 @@ // This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details #include "Luau/Scope.h" -#include "Luau/TypeInfer.h" #include "Luau/Type.h" +#include "Luau/TypeInfer.h" +#include "Luau/TypeReduction.h" #include "Luau/VisitType.h" #include "Fixture.h" diff --git a/tools/faillist.txt b/tools/faillist.txt index 233c75c1..f336bb22 100644 --- a/tools/faillist.txt +++ b/tools/faillist.txt @@ -321,6 +321,7 @@ TypeInfer.globals2 TypeInfer.infer_assignment_value_types_mutable_lval TypeInfer.it_is_ok_to_have_inconsistent_number_of_return_values_in_nonstrict TypeInfer.no_stack_overflow_from_isoptional +TypeInfer.no_stack_overflow_from_isoptional2 TypeInfer.tc_after_error_recovery_no_replacement_name_in_error TypeInfer.tc_if_else_expressions_expected_type_3 TypeInfer.tc_interpolated_string_basic @@ -408,10 +409,7 @@ TypeInferOperators.cannot_compare_tables_that_do_not_have_the_same_metatable TypeInferOperators.cannot_indirectly_compare_types_that_do_not_have_a_metatable TypeInferOperators.cannot_indirectly_compare_types_that_do_not_offer_overloaded_ordering_operators TypeInferOperators.cli_38355_recursive_union -TypeInferOperators.compound_assign_metatable TypeInferOperators.compound_assign_mismatch_metatable -TypeInferOperators.compound_assign_mismatch_op -TypeInferOperators.compound_assign_mismatch_result TypeInferOperators.disallow_string_and_types_without_metatables_from_arithmetic_binary_ops TypeInferOperators.in_nonstrict_mode_strip_nil_from_intersections_when_considering_relational_operators TypeInferOperators.infer_any_in_all_modes_when_lhs_is_unknown From 86494918f51a1cd6290c2616c508bd36cc34b5a3 Mon Sep 17 00:00:00 2001 From: JohnnyMorganz Date: Wed, 11 Jan 2023 16:28:11 +0000 Subject: [PATCH 3/4] Pass string content to autocomplete callback (#800) Closes #718 We pass an extra `contents` parameter to the string callback function to provide contextual information for autocomplete. Because we support both string literals and simple interpolated strings, we pass it through as a `std::string` value. This is a breaking change --- Analysis/include/Luau/Autocomplete.h | 3 ++- Analysis/src/Autocomplete.cpp | 24 ++++++++++++++++++-- tests/Autocomplete.test.cpp | 33 +++++++++++++++++++++++++--- 3 files changed, 54 insertions(+), 6 deletions(-) diff --git a/Analysis/include/Luau/Autocomplete.h b/Analysis/include/Luau/Autocomplete.h index a4101e16..61832577 100644 --- a/Analysis/include/Luau/Autocomplete.h +++ b/Analysis/include/Luau/Autocomplete.h @@ -89,7 +89,8 @@ struct AutocompleteResult }; using ModuleName = std::string; -using StringCompletionCallback = std::function(std::string tag, std::optional ctx)>; +using StringCompletionCallback = + std::function(std::string tag, std::optional ctx, std::optional contents)>; AutocompleteResult autocomplete(Frontend& frontend, const ModuleName& moduleName, Position position, StringCompletionCallback callback); diff --git a/Analysis/src/Autocomplete.cpp b/Analysis/src/Autocomplete.cpp index 49c430e6..9f046b8e 100644 --- a/Analysis/src/Autocomplete.cpp +++ b/Analysis/src/Autocomplete.cpp @@ -1262,6 +1262,23 @@ static bool isSimpleInterpolatedString(const AstNode* node) return interpString != nullptr && interpString->expressions.size == 0; } +static std::optional getStringContents(const AstNode* node) +{ + if (const AstExprConstantString* string = node->as()) + { + return std::string(string->value.data, string->value.size); + } + else if (const AstExprInterpString* interpString = node->as(); interpString && interpString->expressions.size == 0) + { + LUAU_ASSERT(interpString->strings.size == 1); + return std::string(interpString->strings.data->data, interpString->strings.data->size); + } + else + { + return std::nullopt; + } +} + static std::optional autocompleteStringParams(const SourceModule& sourceModule, const ModulePtr& module, const std::vector& nodes, Position position, StringCompletionCallback callback) { @@ -1294,10 +1311,13 @@ static std::optional autocompleteStringParams(const Source return std::nullopt; } - auto performCallback = [&](const FunctionType* funcType) -> std::optional { + std::optional candidateString = getStringContents(nodes.back()); + + auto performCallback = [&](const FunctionType* funcType) -> std::optional + { for (const std::string& tag : funcType->tags) { - if (std::optional ret = callback(tag, getMethodContainingClass(module, candidate->func))) + if (std::optional ret = callback(tag, getMethodContainingClass(module, candidate->func), candidateString)) { return ret; } diff --git a/tests/Autocomplete.test.cpp b/tests/Autocomplete.test.cpp index f241963a..0ac86e82 100644 --- a/tests/Autocomplete.test.cpp +++ b/tests/Autocomplete.test.cpp @@ -18,7 +18,7 @@ LUAU_FASTFLAG(LuauSetMetatableDoesNotTimeTravel) using namespace Luau; -static std::optional nullCallback(std::string tag, std::optional ptr) +static std::optional nullCallback(std::string tag, std::optional ptr, std::optional contents) { return std::nullopt; } @@ -36,9 +36,9 @@ struct ACFixtureImpl : BaseType return Luau::autocomplete(this->frontend, "MainModule", Position{row, column}, nullCallback); } - AutocompleteResult autocomplete(char marker) + AutocompleteResult autocomplete(char marker, StringCompletionCallback callback = nullCallback) { - return Luau::autocomplete(this->frontend, "MainModule", getPosition(marker), nullCallback); + return Luau::autocomplete(this->frontend, "MainModule", getPosition(marker), callback); } CheckResult check(const std::string& source) @@ -3363,4 +3363,31 @@ TEST_CASE_FIXTURE(ACFixture, "type_reduction_is_hooked_up_to_autocomplete") // CHECK("{| x: nil |}" == toString(*ty2, opts)); } +TEST_CASE_FIXTURE(ACFixture, "string_contents_is_available_to_callback") +{ + loadDefinition(R"( + declare function require(path: string): any + )"); + + std::optional require = frontend.typeCheckerForAutocomplete.globalScope->linearSearchForBinding("require"); + REQUIRE(require); + Luau::unfreeze(frontend.typeCheckerForAutocomplete.globalTypes); + attachTag(require->typeId, "RequireCall"); + Luau::freeze(frontend.typeCheckerForAutocomplete.globalTypes); + + check(R"( + local x = require("testing/@1") + )"); + + bool isCorrect = false; + auto ac1 = autocomplete('1', + [&isCorrect](std::string, std::optional, std::optional contents) -> std::optional + { + isCorrect = contents.has_value() && contents.value() == "testing/"; + return std::nullopt; + }); + + CHECK(isCorrect); +} + TEST_SUITE_END(); From 0af10417cd5e4b09fe1a55eb8dbddfb6988737d6 Mon Sep 17 00:00:00 2001 From: JohnnyMorganz Date: Thu, 12 Jan 2023 14:21:25 +0000 Subject: [PATCH 4/4] Attach definition location to `TableType` (#801) Having the location where the table type is defined is useful for tracking other info such as documentation comments, or pointing back through "Go To Definition" functionality etc. This adds in the required info --- Analysis/include/Luau/Type.h | 1 + Analysis/src/Anyification.cpp | 1 + Analysis/src/Clone.cpp | 2 ++ Analysis/src/Instantiation.cpp | 1 + Analysis/src/TypeInfer.cpp | 4 ++++ 5 files changed, 9 insertions(+) diff --git a/Analysis/include/Luau/Type.h b/Analysis/include/Luau/Type.h index 734d40ea..9d4d9940 100644 --- a/Analysis/include/Luau/Type.h +++ b/Analysis/include/Luau/Type.h @@ -375,6 +375,7 @@ struct TableType std::vector instantiatedTypeParams; std::vector instantiatedTypePackParams; ModuleName definitionModuleName; + Location definitionLocation; std::optional boundTo; Tags tags; diff --git a/Analysis/src/Anyification.cpp b/Analysis/src/Anyification.cpp index e0ddeacf..15dd25cc 100644 --- a/Analysis/src/Anyification.cpp +++ b/Analysis/src/Anyification.cpp @@ -59,6 +59,7 @@ TypeId Anyification::clean(TypeId ty) { TableType clone = TableType{ttv->props, ttv->indexer, ttv->level, TableState::Sealed}; clone.definitionModuleName = ttv->definitionModuleName; + clone.definitionLocation = ttv->definitionLocation; clone.name = ttv->name; clone.syntheticName = ttv->syntheticName; clone.tags = ttv->tags; diff --git a/Analysis/src/Clone.cpp b/Analysis/src/Clone.cpp index 870d2949..3dd8df87 100644 --- a/Analysis/src/Clone.cpp +++ b/Analysis/src/Clone.cpp @@ -263,6 +263,7 @@ void TypeCloner::operator()(const TableType& t) arg = clone(arg, dest, cloneState); ttv->definitionModuleName = t.definitionModuleName; + ttv->definitionLocation = t.definitionLocation; ttv->tags = t.tags; } @@ -446,6 +447,7 @@ TypeId shallowClone(TypeId ty, TypeArena& dest, const TxnLog* log, bool alwaysCl LUAU_ASSERT(!ttv->boundTo); TableType clone = TableType{ttv->props, ttv->indexer, ttv->level, ttv->scope, ttv->state}; clone.definitionModuleName = ttv->definitionModuleName; + clone.definitionLocation = ttv->definitionLocation; clone.name = ttv->name; clone.syntheticName = ttv->syntheticName; clone.instantiatedTypeParams = ttv->instantiatedTypeParams; diff --git a/Analysis/src/Instantiation.cpp b/Analysis/src/Instantiation.cpp index 209ba7e9..912c4155 100644 --- a/Analysis/src/Instantiation.cpp +++ b/Analysis/src/Instantiation.cpp @@ -116,6 +116,7 @@ TypeId ReplaceGenerics::clean(TypeId ty) { TableType clone = TableType{ttv->props, ttv->indexer, level, scope, TableState::Free}; clone.definitionModuleName = ttv->definitionModuleName; + clone.definitionLocation = ttv->definitionLocation; return addType(std::move(clone)); } else diff --git a/Analysis/src/TypeInfer.cpp b/Analysis/src/TypeInfer.cpp index 5c1ee388..25fe37ec 100644 --- a/Analysis/src/TypeInfer.cpp +++ b/Analysis/src/TypeInfer.cpp @@ -1535,6 +1535,7 @@ void TypeChecker::check(const ScopePtr& scope, const AstStatTypeAlias& typealias // This is a shallow clone, original recursive links to self are not updated TableType clone = TableType{ttv->props, ttv->indexer, ttv->level, ttv->state}; clone.definitionModuleName = ttv->definitionModuleName; + clone.definitionLocation = ttv->definitionLocation; clone.name = name; for (auto param : binding->typeParams) @@ -2370,6 +2371,7 @@ TypeId TypeChecker::checkExprTable( TableState state = TableState::Unsealed; TableType table = TableType{std::move(props), indexer, scope->level, state}; table.definitionModuleName = currentModuleName; + table.definitionLocation = expr.location; return addType(table); } @@ -5371,6 +5373,7 @@ TypeId TypeChecker::resolveTypeWorker(const ScopePtr& scope, const AstType& anno TableType ttv{props, tableIndexer, scope->level, TableState::Sealed}; ttv.definitionModuleName = currentModuleName; + ttv.definitionLocation = annotation.location; return addType(std::move(ttv)); } else if (const auto& func = annotation.as()) @@ -5572,6 +5575,7 @@ TypeId TypeChecker::instantiateTypeFun(const ScopePtr& scope, const TypeFun& tf, ttv->instantiatedTypeParams = typeParams; ttv->instantiatedTypePackParams = typePackParams; ttv->definitionModuleName = currentModuleName; + ttv->definitionLocation = location; } return instantiated;