From f3b3f3500cc2538126f4840a4e888f584992e23f Mon Sep 17 00:00:00 2001 From: PhoenixWhitefire <86601049+PhoenixWhitefire@users.noreply.github.com> Date: Sun, 12 Jul 2026 17:47:58 +0530 Subject: [PATCH 1/2] do the thing --- Analysis/src/Subtyping.cpp | 9 ++++++++- Analysis/src/TypeChecker2.cpp | 21 ++++++++++++++++++++- tests/TypeInfer.negations.test.cpp | 19 +++++++++++++++++++ 3 files changed, 47 insertions(+), 2 deletions(-) diff --git a/Analysis/src/Subtyping.cpp b/Analysis/src/Subtyping.cpp index 33ce4e0b97..0745b6ae67 100644 --- a/Analysis/src/Subtyping.cpp +++ b/Analysis/src/Subtyping.cpp @@ -31,6 +31,7 @@ LUAU_FASTFLAGVARIABLE(LuauDropUnionSubtypeReasoning) LUAU_FASTFLAGVARIABLE(LuauDontBindOptionalGenericToNil) LUAU_FASTFLAGVARIABLE(LuauImproveUniqueTableWidthSubtyping) LUAU_FASTFLAG(LuauBidirectionalInferenceSimplifyTables) +LUAU_FASTFLAGVARIABLE(LuauNegationsFixSubtypePath) namespace Luau { @@ -1837,6 +1838,7 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const Type TypeId negatedTy = follow(superNegation->ty); SubtypingResult result; + bool moved = false; if (is(negatedTy)) { @@ -1868,6 +1870,7 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const Type { NegationType negatedTmp{ty}; result.andAlso(isCovariantWith(env, subTy, &negatedTmp, scope)); + moved = true; } } } @@ -1885,6 +1888,7 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const Type { NegationType negatedTmp{ty}; result.orElse(isCovariantWith(env, subTy, &negatedTmp, scope)); + moved = true; } } } @@ -1934,7 +1938,10 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const Type else result = {false}; - return result.withSuperComponent(TypePath::TypeField::Negated); + if (FFlag::LuauNegationsFixSubtypePath && moved) + return result; + else + return result.withSuperComponent(TypePath::TypeField::Negated); } SubtypingResult Subtyping::isCovariantWith( diff --git a/Analysis/src/TypeChecker2.cpp b/Analysis/src/TypeChecker2.cpp index 241b91ba93..3f9011b8cf 100644 --- a/Analysis/src/TypeChecker2.cpp +++ b/Analysis/src/TypeChecker2.cpp @@ -40,6 +40,7 @@ LUAU_FASTFLAG(LuauTweakAccessViolationReporting) LUAU_FASTFLAG(LuauReadOnlyIndexers) LUAU_FASTFLAG(LuauImproveUniqueTableWidthSubtyping) LUAU_FASTFLAG(LuauBidirectionalInferenceSimplifyTables) +LUAU_FASTFLAG(LuauNegationsFixSubtypePath) LUAU_FASTFLAG(DebugLuauUserDefinedClasses) @@ -3028,7 +3029,6 @@ Reasonings TypeChecker2::explainReasonings_(TID subTy, TID superTy, Location loc continue; std::optional optSubLeaf = traverse(subTy, reasoning.subPath, builtinTypes, subtyping->arena); - std::optional optSuperLeaf = traverse(superTy, reasoning.superPath, builtinTypes, subtyping->arena); if (!optSubLeaf || !optSuperLeaf) @@ -3064,6 +3064,25 @@ Reasonings TypeChecker2::explainReasonings_(TID subTy, TID superTy, Location loc subLeafAsString = "()"; std::string superLeafAsString = toString(superLeaf); + if (FFlag::LuauNegationsFixSubtypePath && !reasoning.superPath.components.empty()) + { + // If we don't do this, we get "`number` is not a subtype of `number`" etc in our error messages + if (const TypePath::TypeField* tf = get_if(&reasoning.superPath.components.back()); tf && *tf == TypePath::TypeField::Negated) + { + Path clippedPath = reasoning.superPath.pop(); + std::optional optNegationLeaf = traverse(superTy, clippedPath, builtinTypes, subtyping->arena); + + if (optNegationLeaf) + { + const TypeOrPack& negationLeaf = *optNegationLeaf; + auto negationLeafTy = get(negationLeaf); + + if (negationLeafTy) + superLeafAsString = toString(negationLeaf); + } + } + } + // if the string is empty, it must be an empty type pack if (superLeafAsString.empty()) superLeafAsString = "()"; diff --git a/tests/TypeInfer.negations.test.cpp b/tests/TypeInfer.negations.test.cpp index 8fc8ea276f..00c34a6219 100644 --- a/tests/TypeInfer.negations.test.cpp +++ b/tests/TypeInfer.negations.test.cpp @@ -7,6 +7,8 @@ #include "Luau/Common.h" #include "ScopedFlags.h" +LUAU_FASTFLAG(LuauNegationsFixSubtypePath) + using namespace Luau; namespace @@ -77,4 +79,21 @@ end LUAU_REQUIRE_NO_ERRORS(result); } +TEST_CASE_FIXTURE(NegationFixture, "subtype_path_is_valid_for_unions") +{ + ScopedFastFlag fixSubtypePath{FFlag::LuauNegationsFixSubtypePath, true}; + + CheckResult result = check(R"( + type T = Not + local x: T = false :: false + )"); + + LUAU_REQUIRE_ERROR_COUNT(1, result); + CHECK_EQ( + "Expected this to be '~(false?)', but got 'false'; \n" + "the negation `~(false?)`, and `false` is not a subtype of `~(false?)`", + toString(result.errors[0]) + ); +} + TEST_SUITE_END(); From 5fa086ce90eb7b08376aafd37ea1bd590b001d8f Mon Sep 17 00:00:00 2001 From: PhoenixWhitefire <86601049+PhoenixWhitefire@users.noreply.github.com> Date: Sun, 12 Jul 2026 18:16:29 +0530 Subject: [PATCH 2/2] Add intersection test --- tests/TypeInfer.negations.test.cpp | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/TypeInfer.negations.test.cpp b/tests/TypeInfer.negations.test.cpp index 00c34a6219..92f3f4810e 100644 --- a/tests/TypeInfer.negations.test.cpp +++ b/tests/TypeInfer.negations.test.cpp @@ -96,4 +96,21 @@ TEST_CASE_FIXTURE(NegationFixture, "subtype_path_is_valid_for_unions") ); } +TEST_CASE_FIXTURE(NegationFixture, "subtype_path_is_valid_for_intersections") +{ + ScopedFastFlag fixSubtypePath{FFlag::LuauNegationsFixSubtypePath, true}; + + CheckResult result = check(R"( + type T = Not + local x: T = false + )"); + + LUAU_REQUIRE_ERROR_COUNT(1, result); + CHECK_EQ( + "Expected this to be '~(boolean & unknown)', but got 'boolean'; \n" + "the negation `~(boolean & unknown)`, and `boolean` is not a subtype of `~(boolean & unknown)`", + toString(result.errors[0]) + ); +} + TEST_SUITE_END();