Skip to content

Commit de1727b

Browse files
committed
Introduce -Xpatmat-analysis-timeout
1 parent 3408ed7 commit de1727b

File tree

8 files changed

+3140
-6
lines changed

8 files changed

+3140
-6
lines changed

compiler/src/dotty/tools/dotc/Run.scala

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,30 @@ class Run(comp: Compiler, ictx: Context) extends ImplicitRunInfo with Constraint
6262
*/
6363
@volatile var isCancelled = false
6464

65+
/** The timeout for pattern match exhaustivity analysis, in ms.
66+
* When the timeout is reached, it is reduced for the next analysis (see "backoff").
67+
* When the timeout is not reached, it is recovered (up to the original, see "recover").
68+
* */
69+
private var myExhaustivityAnalysisTimeout: Int =
70+
Int.MinValue // sentinel value; means whatever is set in command line option
71+
72+
def exhaustivityAnalysisTimeout: Int =
73+
if myExhaustivityAnalysisTimeout == Int.MinValue
74+
then ctx.settings.XpatmatAnalysisTimeout.value
75+
else myExhaustivityAnalysisTimeout
76+
77+
/** Exponentially back off, by halving on every timeout, to a minimum 100 ms. */
78+
def backoffExhaustivityAnalysisTimeout(): Unit =
79+
myExhaustivityAnalysisTimeout =
80+
(exhaustivityAnalysisTimeout / 2)
81+
.max(100)
82+
83+
/** Recover slowly, by 1.5 times, up to the original value. */
84+
def recoverExhaustivityAnalysisTimeout(): Unit =
85+
myExhaustivityAnalysisTimeout =
86+
(exhaustivityAnalysisTimeout * 1.5).toInt
87+
.min(ictx.settings.XpatmatAnalysisTimeout.value)
88+
6589
private var compiling = false
6690

6791
private var myUnits: List[CompilationUnit] = Nil

compiler/src/dotty/tools/dotc/config/ScalaSettings.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,7 @@ private sealed trait XSettings:
335335
val XverifySignatures: Setting[Boolean] = BooleanSetting(AdvancedSetting, "Xverify-signatures", "Verify generic signatures in generated bytecode.")
336336
val XignoreScala2Macros: Setting[Boolean] = BooleanSetting(AdvancedSetting, "Xignore-scala2-macros", "Ignore errors when compiling code that calls Scala2 macros, these will fail at runtime.")
337337
val XimportSuggestionTimeout: Setting[Int] = IntSetting(AdvancedSetting, "Ximport-suggestion-timeout", "Timeout (in ms) for searching for import suggestions when errors are reported.", 8000)
338+
val XpatmatAnalysisTimeout: Setting[Int] = IntSetting(AdvancedSetting, "Xpatmat-analysis-timeout", "Timeout (in ms) for match analysis.", 8 * 1000) // 8s
338339
val Xsemanticdb: Setting[Boolean] = BooleanSetting(AdvancedSetting, "Xsemanticdb", "Store information in SemanticDB.", aliases = List("-Ysemanticdb"))
339340
val XuncheckedJavaOutputVersion: Setting[String] = ChoiceSetting(AdvancedSetting, "Xunchecked-java-output-version", "target", "Emit bytecode for the specified version of the Java platform. This might produce bytecode that will break at runtime. Corresponds to -target flag in javac. When on JDK 9+, consider -java-output-version as a safer alternative.", ScalaSettingsProperties.supportedTargetVersions, "", aliases = List("-Xtarget", "--Xtarget"))
340341
val XcheckMacros: Setting[Boolean] = BooleanSetting(AdvancedSetting, "Xcheck-macros", "Check some invariants of macro generated code while expanding macros", aliases = List("--Xcheck-macros"))

compiler/src/dotty/tools/dotc/printing/Formatting.scala

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,13 +115,15 @@ object Formatting {
115115
given Show[Char] = ShowAny
116116
given Show[Boolean] = ShowAny
117117
given Show[Integer] = ShowAny
118+
given Show[Long] = ShowAny
118119
given Show[String] = ShowAny
119120
given Show[Class[?]] = ShowAny
120121
given Show[Throwable] = ShowAny
121122
given Show[StringBuffer] = ShowAny
122123
given Show[CompilationUnit] = ShowAny
123124
given Show[Phases.Phase] = ShowAny
124125
given Show[TyperState] = ShowAny
126+
given Show[Unit] = ShowAny
125127
given Show[config.ScalaVersion] = ShowAny
126128
given Show[io.AbstractFile] = ShowAny
127129
given Show[parsing.Scanners.Scanner] = ShowAny

compiler/src/dotty/tools/dotc/reporting/trace.scala

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ trait TraceSyntax:
9696
(op: => T)(using Context): T =
9797
if ctx.mode.is(Mode.Printing) || !isForced && (printer eq Printers.noPrinter) then op
9898
else
99+
val start = System.nanoTime
99100
// Avoid evaluating question multiple time, since each evaluation
100101
// may cause some extra logging output.
101102
val q = question
@@ -109,7 +110,13 @@ trait TraceSyntax:
109110
def finalize(msg: String) =
110111
if !finalized then
111112
ctx.base.indent -= 1
112-
doLog(s"$margin$msg")
113+
val stop = System.nanoTime
114+
val diffNs = stop - start
115+
val diffS = (diffNs / 1000 / 1000).toInt / 1000.0
116+
if diffS > 0.1 then
117+
doLog(s"$margin$msg (${"%.2f".format(diffS)} s)")
118+
else
119+
doLog(s"$margin$msg")
113120
finalized = true
114121
try
115122
doLog(s"$margin$leading")

compiler/src/dotty/tools/dotc/transform/patmat/Space.scala

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,20 @@ case class Prod(tp: Type, unappTp: TermRef, params: List[Space]) extends Space
111111
case class Or(spaces: Seq[Space]) extends Space
112112

113113
object SpaceEngine {
114+
val DeadlineKey = new Property.Key[Long]
115+
116+
def setupDeadline(using Context): Context =
117+
val timeout = ctx.run.nn.exhaustivityAnalysisTimeout
118+
if timeout >= 0 then
119+
ctx.fresh.setProperty(DeadlineKey, System.currentTimeMillis() + timeout)
120+
else
121+
ctx
122+
123+
def isPastDeadline(using Context): Boolean =
124+
ctx.property(DeadlineKey) match
125+
case Some(deadline) => System.currentTimeMillis() > deadline
126+
case _ => false
127+
114128
def simplify(space: Space)(using Context): Space = space.simplify
115129
def isSubspace(a: Space, b: Space)(using Context): Boolean = a.isSubspace(b)
116130
def canDecompose(typ: Typ)(using Context): Boolean = typ.canDecompose
@@ -590,6 +604,7 @@ object SpaceEngine {
590604

591605
/** Whether the extractor covers the given type */
592606
def covers(unapp: TermRef, scrutineeTp: Type, argLen: Int)(using Context): Boolean = trace(i"covers($unapp, $scrutineeTp, $argLen)") {
607+
!isPastDeadline && (
593608
SpaceEngine.isIrrefutable(unapp, argLen)
594609
|| unapp.symbol == defn.TypeTest_unapply && {
595610
val AppliedType(_, _ :: tp :: Nil) = unapp.prefix.widen.dealias: @unchecked
@@ -599,6 +614,7 @@ object SpaceEngine {
599614
val AppliedType(_, tp :: Nil) = unapp.prefix.widen.dealias: @unchecked
600615
scrutineeTp <:< tp
601616
}
617+
)
602618
}
603619

604620
/** Decompose a type into subspaces -- assume the type can be decomposed */
@@ -666,13 +682,16 @@ object SpaceEngine {
666682

667683
val parts = children.map { sym =>
668684
val sym1 = if (sym.is(ModuleClass)) sym.sourceModule else sym
669-
val refined = trace(i"refineUsingParent($tp, $sym1, $mixins)")(TypeOps.refineUsingParent(tp, sym1, mixins))
685+
val refined =
686+
if isPastDeadline then NoType
687+
else TypeOps.refineUsingParent(tp, sym1, mixins)
670688

671689
def inhabited(tp: Type): Boolean = tp.dealias match
672690
case AndType(tp1, tp2) => !TypeComparer.provablyDisjoint(tp1, tp2)
673691
case OrType(tp1, tp2) => inhabited(tp1) || inhabited(tp2)
674692
case tp: RefinedType => inhabited(tp.parent)
675693
case tp: TypeRef => inhabited(tp.prefix)
694+
case NoType => false
676695
case _ => true
677696

678697
if inhabited(refined) then refined
@@ -862,6 +881,21 @@ object SpaceEngine {
862881
case _ => tp
863882
})
864883

884+
def checkExhaustivityInDeadline(m: Match)(using Context): Unit = {
885+
inContext(setupDeadline):
886+
checkExhaustivity(m)
887+
if isPastDeadline then
888+
ctx.run.nn.backoffExhaustivityAnalysisTimeout()
889+
val setting = ctx.settings.XpatmatAnalysisTimeout
890+
report.warning(
891+
em"""Match analysis requires more time than allowed. You can try:
892+
| * doubling the timeout: ${setting.name}:${setting.value * 2}
893+
| * disabling the timeout: ${setting.name}:-1
894+
| * adding `: @unchecked` to the scrutinee""", m.srcPos)
895+
else
896+
ctx.run.nn.recoverExhaustivityAnalysisTimeout()
897+
}
898+
865899
def checkExhaustivity(m: Match)(using Context): Unit = trace(i"checkExhaustivity($m)") {
866900
val selTyp = toUnderlying(m.selector.tpe.stripUnsafeNulls()).dealias
867901
val targetSpace = trace(i"targetSpace($selTyp)")(project(selTyp))
@@ -880,7 +914,8 @@ object SpaceEngine {
880914

881915
if uncovered.nonEmpty then
882916
val deduped = dedup(uncovered)
883-
report.warning(PatternMatchExhaustivity(deduped, m), m.selector)
917+
if !isPastDeadline then
918+
report.warning(PatternMatchExhaustivity(deduped, m), m.selector)
884919
}
885920

886921
private def reachabilityCheckable(sel: Tree)(using Context): Boolean =
@@ -929,7 +964,8 @@ object SpaceEngine {
929964
then {
930965
val nullOnly = isNullable && i == len - 1 && isWildcardArg(pat)
931966
val msg = if nullOnly then MatchCaseOnlyNullWarning() else MatchCaseUnreachable()
932-
report.warning(msg, pat.srcPos)
967+
if !isPastDeadline then
968+
report.warning(msg, pat.srcPos)
933969
}
934970
deferred = Nil
935971
}
@@ -941,6 +977,6 @@ object SpaceEngine {
941977
}
942978

943979
def checkMatch(m: Match)(using Context): Unit =
944-
if exhaustivityCheckable(m.selector) then checkExhaustivity(m)
980+
if exhaustivityCheckable(m.selector) then checkExhaustivityInDeadline(m)
945981
if reachabilityCheckable(m.selector) then checkReachability(m)
946982
}

compiler/src/dotty/tools/dotc/typer/ImportSuggestions.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -264,7 +264,7 @@ trait ImportSuggestions:
264264
end importSuggestions
265265

266266
/** Reduce next timeout for import suggestions by the amount of time it took
267-
* for current search, but but never less than to half of the previous budget.
267+
* for current search, but never less than to half of the previous budget.
268268
*/
269269
private def reduceTimeBudget(used: Int)(using Context) =
270270
val run = ctx.run.nn

0 commit comments

Comments
 (0)