From 8c08568afa68add27b57f025087e8de4cd0dca14 Mon Sep 17 00:00:00 2001 From: Dale Wijnand Date: Sun, 16 Jul 2023 14:09:12 +0100 Subject: [PATCH 1/4] Strip LazyRef before calling simplified, in MT reduction --- .../dotty/tools/dotc/core/TypeComparer.scala | 15 ++- tests/pos/i18175.scala | 106 ++++++++++++++++++ 2 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 tests/pos/i18175.scala diff --git a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala index db1bf85ade93..52bc48bcd95f 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -3248,7 +3248,20 @@ class TrackingTypeComparer(initctx: Context) extends TypeComparer(initctx) { MatchTypeTrace.noInstance(scrut, cas, fails) NoType case MatchResult.Reduced(tp) => - tp.simplified + // A recursive match type will have the recursive call + // wrapped in a LazyRef. For example in i18175, the recursive calls + // to IsPiped within the definition of IsPiped are all wrapped in LazyRefs. + // In addition to that, TypeMaps, such as the one that backs TypeOps.simplify, + // by default will rewrap a LazyRef when applying its function. + // The result of those two things means that given a big enough input + // that recursive enough times through one or multiple match types, + // reducing and simplifying the result of the case bodies, + // can end up with a large stack of directly-nested lazy refs. + // And if that nesting level breaches `Config.LogPendingSubTypesThreshold`, + // then TypeComparer will eventually start returning `false` for `isSubType`. + // Or, under -Yno-deep-subtypes, start throwing AssertionErrors. + // So, we eagerly strip that lazy ref here to avoid the stacking. + tp.stripLazyRef.simplified case Nil => val casesText = MatchTypeTrace.noMatchesText(scrut, cases) ErrorType(reporting.MatchTypeNoCases(casesText)) diff --git a/tests/pos/i18175.scala b/tests/pos/i18175.scala new file mode 100644 index 000000000000..2480ddccc320 --- /dev/null +++ b/tests/pos/i18175.scala @@ -0,0 +1,106 @@ +import scala.compiletime.ops.int.{ +, -, Max } +import scala.compiletime.ops.string.{ Substring, Length, Matches, CharAt } + +class Regex[P] private() extends Serializable: + def unapply(s: CharSequence)(implicit n: Regex.Sanitizer[P]): Option[P] = ??? + +object Regex: + def apply[R <: String & Singleton](regex: R): Regex[Compile[R]] = ??? + + abstract class Sanitizer[T] + object Sanitizer: + given Sanitizer[EmptyTuple] = ??? + given stringcase[T <: Tuple: Sanitizer]: Sanitizer[String *: T] = ??? + given optioncase[T <: Tuple: Sanitizer]: Sanitizer[Option[String] *: T] = ??? + given Sanitizer[String] = ??? + given Sanitizer[Option[String]] = ??? + + type Compile[R <: String] = Matches["", R] match + case _ => Reverse[EmptyTuple, Loop[R, 0, Length[R], EmptyTuple, IsPiped[R, 0, Length[R], 0]]] + + type Loop[R <: String, Lo <: Int, Hi <: Int, Acc <: Tuple, Opt <: Int] <: Tuple = Lo match + case Hi => Acc + case _ => CharAt[R, Lo] match + case '\\' => CharAt[R, Lo + 1] match + case 'Q' => Loop[R, ToClosingQE[R, Lo + 2], Hi, Acc, Opt] + case _ => Loop[R, Lo + 2, Hi, Acc, Opt] + case '[' => Loop[R, ToClosingBracket[R, Lo + 1, 0], Hi, Acc, Opt] + case ')' => Loop[R, Lo + 1, Hi, Acc, Max[0, Opt - 1]] + case '(' => Opt match + case 0 => IsMarked[R, ToClosingParenthesis[R, Lo + 1, 0], Hi] match + case true => IsCapturing[R, Lo + 1] match + case false => Loop[R, Lo + 1, Hi, Acc, 1] + case true => Loop[R, Lo + 1, Hi, Option[String] *: Acc, 1] + case false => IsCapturing[R, Lo + 1] match + case false => Loop[R, Lo + 1, Hi, Acc, IsPiped[R, Lo + 1, Hi, 0]] + case true => Loop[R, Lo + 1, Hi, String *: Acc, IsPiped[R, Lo + 1, Hi, 0]] + case _ => IsCapturing[R, Lo + 1] match + case false => Loop[R, Lo + 1, Hi, Acc, Opt + 1] + case true => Loop[R, Lo + 1, Hi, Option[String] *: Acc, Opt + 1] + case _ => Loop[R, Lo + 1, Hi, Acc, Opt] + + type IsCapturing[R <: String, At <: Int] <: Boolean = CharAt[R, At] match + case '?' => CharAt[R, At + 1] match + case '<' => CharAt[R, At + 2] match + case '=' | '!' => false + case _ => true + case _ => false + case _ => true + + type IsMarked[R <: String, At <: Int, Hi <: Int] <: Boolean = At match + case Hi => false + case _ => CharAt[R, At] match + case '?' | '*' => true + case '{' => CharAt[R, At + 1] match + case '0' => true + case _ => false + case _ => false + + type IsPiped[R <: String, At <: Int, Hi <: Int, Lvl <: Int] <: Int = At match + case Hi => 0 + case _ => CharAt[R, At] match + case '\\' => CharAt[R, At + 1] match + case 'Q' => IsPiped[R, ToClosingQE[R, At + 2], Hi, Lvl] + case _ => IsPiped[R, At + 2, Hi, Lvl] + case '[' => IsPiped[R, ToClosingBracket[R, At + 1, 0], Hi, Lvl] + case '(' => IsPiped[R, ToClosingParenthesis[R, At + 1, 0], Hi, Lvl + 1] + case '|' => 1 + case ')' => 0 + case _ => IsPiped[R, At + 1, Hi, Lvl] + + type ToClosingParenthesis[R <: String, At <: Int, Lvl <: Int] <: Int = CharAt[R, At] match + case '\\' => CharAt[R, At + 1] match + case 'Q' => ToClosingParenthesis[R, ToClosingQE[R, At + 2], Lvl] + case _ => ToClosingParenthesis[R, At + 2, Lvl] + case '[' => ToClosingParenthesis[R, ToClosingBracket[R, At + 1, 0], Lvl] + case ')' => Lvl match + case 0 => At + 1 + case _ => ToClosingParenthesis[R, At + 1, Lvl - 1] + case '(' => ToClosingParenthesis[R, At + 1, Lvl + 1] + case _ => ToClosingParenthesis[R, At + 1, Lvl] + + type ToClosingBracket[R <: String, At <: Int, Lvl <: Int] <: Int = CharAt[R, At] match + case '\\' => CharAt[R, At + 1] match + case 'Q' => ToClosingBracket[R, ToClosingQE[R, At + 2], Lvl] + case _ => ToClosingBracket[R, At + 2, Lvl] + case '[' => ToClosingBracket[R, At + 1, Lvl + 1] + case ']' => Lvl match + case 0 => At + 1 + case _ => ToClosingBracket[R, At + 1, Lvl - 1] + case _ => ToClosingBracket[R, At + 1, Lvl] + + type ToClosingQE[R <: String, At <: Int] <: Int = CharAt[R, At] match + case '\\' => CharAt[R, At + 1] match + case 'E' => At + 2 + case _ => ToClosingQE[R, At + 2] + case _ => ToClosingQE[R, At + 1] + + type Reverse[Acc <: Tuple, X <: Tuple] <: Tuple = X match + case x *: xs => Reverse[x *: Acc, xs] + case EmptyTuple => Acc + +object Test: + def main(args: Array[String]): Unit = + val r75 = Regex("(x|y|z[QW])*(longish|loquatious|excessive|overblown[QW])*") + "xyzQzWlongishoverblownW" match + case r75((Some(g0), Some(g1))) => ??? // failure From d087c435dc2e5a9f77dce08adb1ab2989aa7e778 Mon Sep 17 00:00:00 2001 From: Dale Wijnand Date: Mon, 17 Jul 2023 18:37:31 +0100 Subject: [PATCH 2/4] Move stripLazyRef inside simplified --- compiler/src/dotty/tools/dotc/core/TypeComparer.scala | 2 +- compiler/src/dotty/tools/dotc/core/Types.scala | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala index 52bc48bcd95f..0d2307cd8178 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -3261,7 +3261,7 @@ class TrackingTypeComparer(initctx: Context) extends TypeComparer(initctx) { // then TypeComparer will eventually start returning `false` for `isSubType`. // Or, under -Yno-deep-subtypes, start throwing AssertionErrors. // So, we eagerly strip that lazy ref here to avoid the stacking. - tp.stripLazyRef.simplified + tp.simplified case Nil => val casesText = MatchTypeTrace.noMatchesText(scrut, cases) ErrorType(reporting.MatchTypeNoCases(casesText)) diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index 166f65533584..47c44d0786a0 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -1984,7 +1984,11 @@ object Types { * this method handles this by never simplifying inside a `MethodicType`, * except for replacing type parameters with associated type variables. */ - def simplified(using Context): Type = TypeOps.simplify(this, null) + def simplified(using Context): Type = + // stripping LazyRef is important for the reduction of applied match types + // see the comment in matchCases/recur for more details + val tp = stripLazyRef + TypeOps.simplify(tp, null) /** Compare `this == that`, assuming corresponding binders in `bs` are equal. * The normal `equals` should be equivalent to `equals(that, null`)`. From d42301effb84347cf60aae0b1a249ca2f4c826d7 Mon Sep 17 00:00:00 2001 From: Dale Wijnand Date: Mon, 17 Jul 2023 22:42:38 +0100 Subject: [PATCH 3/4] Move comment for stripLazyRef usage --- .../src/dotty/tools/dotc/core/TypeComparer.scala | 13 ------------- compiler/src/dotty/tools/dotc/core/Types.scala | 15 +++++++++++++-- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala index 0d2307cd8178..db1bf85ade93 100644 --- a/compiler/src/dotty/tools/dotc/core/TypeComparer.scala +++ b/compiler/src/dotty/tools/dotc/core/TypeComparer.scala @@ -3248,19 +3248,6 @@ class TrackingTypeComparer(initctx: Context) extends TypeComparer(initctx) { MatchTypeTrace.noInstance(scrut, cas, fails) NoType case MatchResult.Reduced(tp) => - // A recursive match type will have the recursive call - // wrapped in a LazyRef. For example in i18175, the recursive calls - // to IsPiped within the definition of IsPiped are all wrapped in LazyRefs. - // In addition to that, TypeMaps, such as the one that backs TypeOps.simplify, - // by default will rewrap a LazyRef when applying its function. - // The result of those two things means that given a big enough input - // that recursive enough times through one or multiple match types, - // reducing and simplifying the result of the case bodies, - // can end up with a large stack of directly-nested lazy refs. - // And if that nesting level breaches `Config.LogPendingSubTypesThreshold`, - // then TypeComparer will eventually start returning `false` for `isSubType`. - // Or, under -Yno-deep-subtypes, start throwing AssertionErrors. - // So, we eagerly strip that lazy ref here to avoid the stacking. tp.simplified case Nil => val casesText = MatchTypeTrace.noMatchesText(scrut, cases) diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index 47c44d0786a0..50ef65b3327a 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -1985,8 +1985,19 @@ object Types { * except for replacing type parameters with associated type variables. */ def simplified(using Context): Type = - // stripping LazyRef is important for the reduction of applied match types - // see the comment in matchCases/recur for more details + // A recursive match type will have the recursive call + // wrapped in a LazyRef. For example in i18175, the recursive calls + // to IsPiped within the definition of IsPiped are all wrapped in LazyRefs. + // In addition to that, TypeMaps, such as the one that backs TypeOps.simplify, + // by default will rewrap a LazyRef when applying its function. + // The result of those two things means that given a big enough input + // that recursive enough times through one or multiple match types, + // reducing and simplifying the result of the case bodies, + // can end up with a large stack of directly-nested lazy refs. + // And if that nesting level breaches `Config.LogPendingSubTypesThreshold`, + // then TypeComparer will eventually start returning `false` for `isSubType`. + // Or, under -Yno-deep-subtypes, start throwing AssertionErrors. + // So, we eagerly strip that lazy ref here to avoid the stacking. val tp = stripLazyRef TypeOps.simplify(tp, null) From 990b65c4a5154dea2346f3f9a069fe5cf8441455 Mon Sep 17 00:00:00 2001 From: Dale Wijnand Date: Tue, 18 Jul 2023 13:15:15 +0100 Subject: [PATCH 4/4] Fix comment Co-authored-by: Guillaume Martres --- compiler/src/dotty/tools/dotc/core/Types.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index 50ef65b3327a..064eac386a23 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -1991,7 +1991,7 @@ object Types { // In addition to that, TypeMaps, such as the one that backs TypeOps.simplify, // by default will rewrap a LazyRef when applying its function. // The result of those two things means that given a big enough input - // that recursive enough times through one or multiple match types, + // that recurses enough times through one or multiple match types, // reducing and simplifying the result of the case bodies, // can end up with a large stack of directly-nested lazy refs. // And if that nesting level breaches `Config.LogPendingSubTypesThreshold`,