From 5848a912c960a4468cea4022abbc1ab79a93cb1e Mon Sep 17 00:00:00 2001 From: Pyseph <45090858+Pyseph@users.noreply.github.com> Date: Sat, 26 Sep 2026 15:39:21 +0200 Subject: [PATCH 1/2] Skip subtyping reasoning that is never read A successful SubtypingResult's reasoning is never read, yet every with*Component/Path call built TypePath entries for it, and T <: A | B merged each failing option's reasoning only to clear it. Both are skipped under LuauSubtypingSkipUnreadReasoning; failing results, and so error messages, are unchanged. --- Analysis/src/Subtyping.cpp | 30 +++++++++++++++++++++++++++++- tests/Subtyping.test.cpp | 11 +++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/Analysis/src/Subtyping.cpp b/Analysis/src/Subtyping.cpp index 3dbf12b48..237860ee4 100644 --- a/Analysis/src/Subtyping.cpp +++ b/Analysis/src/Subtyping.cpp @@ -31,6 +31,7 @@ LUAU_FASTFLAG(LuauRefactorStringSemanticSubtyping) LUAU_FASTFLAG(DebugLuauExactTableTypes) LUAU_FASTFLAGVARIABLE(LuauFixSuperNegationTypePaths) LUAU_FASTFLAGVARIABLE(LuauDoNotIceForBindingGeneric) +LUAU_FASTFLAGVARIABLE(LuauSubtypingSkipUnreadReasoning) namespace Luau @@ -304,6 +305,11 @@ SubtypingResult& SubtypingResult::withBothComponent(TypePath::Component componen SubtypingResult& SubtypingResult::withSubComponent(TypePath::Component component) { + // Nothing reads the reasoning of a successful result: andAlso and orElse drop it, and negate + // discards it. + if (FFlag::LuauSubtypingSkipUnreadReasoning && isSubtype) + return *this; + if (reasoning.empty()) reasoning.insert(SubtypingReasoning{Path(std::move(component)), TypePath::kEmpty}); else @@ -317,6 +323,9 @@ SubtypingResult& SubtypingResult::withSubComponent(TypePath::Component component SubtypingResult& SubtypingResult::withSuperComponent(TypePath::Component component) { + if (FFlag::LuauSubtypingSkipUnreadReasoning && isSubtype) + return *this; + if (reasoning.empty()) reasoning.insert(SubtypingReasoning{TypePath::kEmpty, Path(std::move(component))}); else @@ -335,6 +344,9 @@ SubtypingResult& SubtypingResult::withBothPath(TypePath::Path path) SubtypingResult& SubtypingResult::withSubPath(TypePath::Path path) { + if (FFlag::LuauSubtypingSkipUnreadReasoning && isSubtype) + return *this; + if (reasoning.empty()) reasoning.insert(SubtypingReasoning{std::move(path), TypePath::kEmpty}); else @@ -348,6 +360,9 @@ SubtypingResult& SubtypingResult::withSubPath(TypePath::Path path) SubtypingResult& SubtypingResult::withSuperPath(TypePath::Path path) { + if (FFlag::LuauSubtypingSkipUnreadReasoning && isSubtype) + return *this; + if (reasoning.empty()) reasoning.insert(SubtypingReasoning{TypePath::kEmpty, std::move(path)}); else @@ -1532,6 +1547,9 @@ template SubtypingResult Subtyping::isContravariantWith(SubtypingEnvironment& env, SubTy subTy, SuperTy superTy, NotNull scope) { SubtypingResult result = isCovariantWith(env, superTy, subTy, scope); + if (FFlag::LuauSubtypingSkipUnreadReasoning && result.isSubtype) + return result; + if (result.reasoning.empty()) result.reasoning.insert(SubtypingReasoning{TypePath::kEmpty, TypePath::kEmpty, SubtypingVariance::Contravariant}); else @@ -1564,6 +1582,9 @@ SubtypingResult Subtyping::isInvariantWith(SubtypingEnvironment& env, SubTy subT SubtypingResult result = isCovariantWith(env, subTy, superTy, scope); result.andAlso(isContravariantWith(env, subTy, superTy, scope)); + if (FFlag::LuauSubtypingSkipUnreadReasoning && result.isSubtype) + return result; + if (result.reasoning.empty()) result.reasoning.insert(SubtypingReasoning{TypePath::kEmpty, TypePath::kEmpty, SubtypingVariance::Invariant}); else @@ -1651,7 +1672,14 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypeId sub if (next.isSubtype) return next; - result.andAlso(next.withSuperComponent(TypePath::Index{index, TypePath::Index::Variant::Union})); + if (FFlag::LuauSubtypingSkipUnreadReasoning) + { + // The reasoning is cleared below, so merging each option's into it is wasted work. + next.reasoning.clear(); + result.andAlso(std::move(next)); + } + else + result.andAlso(next.withSuperComponent(TypePath::Index{index, TypePath::Index::Variant::Union})); ++index; } diff --git a/tests/Subtyping.test.cpp b/tests/Subtyping.test.cpp index f351a4356..6bac9e392 100644 --- a/tests/Subtyping.test.cpp +++ b/tests/Subtyping.test.cpp @@ -25,6 +25,8 @@ LUAU_FASTFLAG(LuauNewTypePathErrorMessages) LUAU_FASTFLAG(LuauRefactorStringSemanticSubtyping) LUAU_FASTFLAG(DebugLuauParseExactTables) LUAU_FASTFLAG(DebugLuauExactTableTypes) +LUAU_FASTFLAG(LuauFixSuperNegationTypePaths) +LUAU_FASTFLAG(LuauSubtypingSkipUnreadReasoning) using namespace Luau; @@ -2251,6 +2253,15 @@ TEST_CASE_FIXTURE(SubtypeFixture, "multiple_reasonings") ); } +TEST_CASE_FIXTURE(SubtypeFixture, "successful_subtyping_has_no_reasoning") +{ + ScopedFastFlag sffs[] = {{FFlag::LuauFixSuperNegationTypePaths, true}, {FFlag::LuauSubtypingSkipUnreadReasoning, true}}; + + SubtypingResult result = isSubtype(getBuiltins()->numberType, negate(getBuiltins()->stringType)); + CHECK(result.isSubtype); + CHECK(result.reasoning.empty()); +} + TEST_CASE_FIXTURE(SubtypeFixture, "substitute_a_generic_for_a_negation") { // (x: A, y: B) -> (A & ~(false?)) | B From 1edda12c17e8dee9c3cadb2ae8db4f677ee48036 Mon Sep 17 00:00:00 2001 From: Pyseph <45090858+Pyseph@users.noreply.github.com> Date: Tue, 29 Sep 2026 00:32:04 +0200 Subject: [PATCH 2/2] Reword the union reasoning comment and drop an unneeded test flag --- Analysis/src/Subtyping.cpp | 2 +- tests/Subtyping.test.cpp | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Analysis/src/Subtyping.cpp b/Analysis/src/Subtyping.cpp index 237860ee4..153beb0a0 100644 --- a/Analysis/src/Subtyping.cpp +++ b/Analysis/src/Subtyping.cpp @@ -1674,7 +1674,7 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypeId sub if (FFlag::LuauSubtypingSkipUnreadReasoning) { - // The reasoning is cleared below, so merging each option's into it is wasted work. + // The failed reasoning emitted when subtyping `T <: A | B | C` is always cleared before returning, so we can simply skip its inclusion. next.reasoning.clear(); result.andAlso(std::move(next)); } diff --git a/tests/Subtyping.test.cpp b/tests/Subtyping.test.cpp index 6bac9e392..17ee60a0d 100644 --- a/tests/Subtyping.test.cpp +++ b/tests/Subtyping.test.cpp @@ -25,7 +25,6 @@ LUAU_FASTFLAG(LuauNewTypePathErrorMessages) LUAU_FASTFLAG(LuauRefactorStringSemanticSubtyping) LUAU_FASTFLAG(DebugLuauParseExactTables) LUAU_FASTFLAG(DebugLuauExactTableTypes) -LUAU_FASTFLAG(LuauFixSuperNegationTypePaths) LUAU_FASTFLAG(LuauSubtypingSkipUnreadReasoning) using namespace Luau; @@ -2255,7 +2254,7 @@ TEST_CASE_FIXTURE(SubtypeFixture, "multiple_reasonings") TEST_CASE_FIXTURE(SubtypeFixture, "successful_subtyping_has_no_reasoning") { - ScopedFastFlag sffs[] = {{FFlag::LuauFixSuperNegationTypePaths, true}, {FFlag::LuauSubtypingSkipUnreadReasoning, true}}; + ScopedFastFlag sff{FFlag::LuauSubtypingSkipUnreadReasoning, true}; SubtypingResult result = isSubtype(getBuiltins()->numberType, negate(getBuiltins()->stringType)); CHECK(result.isSubtype);