Skip to content

Commit

Permalink
Introduce -Xpatmat-analysis-timeout
Browse files Browse the repository at this point in the history
  • Loading branch information
dwijnand committed Oct 14, 2024
1 parent 3408ed7 commit de1727b
Show file tree
Hide file tree
Showing 8 changed files with 3,140 additions and 6 deletions.
24 changes: 24 additions & 0 deletions compiler/src/dotty/tools/dotc/Run.scala
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,30 @@ class Run(comp: Compiler, ictx: Context) extends ImplicitRunInfo with Constraint
*/
@volatile var isCancelled = false

/** The timeout for pattern match exhaustivity analysis, in ms.
* When the timeout is reached, it is reduced for the next analysis (see "backoff").
* When the timeout is not reached, it is recovered (up to the original, see "recover").
* */
private var myExhaustivityAnalysisTimeout: Int =
Int.MinValue // sentinel value; means whatever is set in command line option

def exhaustivityAnalysisTimeout: Int =
if myExhaustivityAnalysisTimeout == Int.MinValue
then ctx.settings.XpatmatAnalysisTimeout.value
else myExhaustivityAnalysisTimeout

/** Exponentially back off, by halving on every timeout, to a minimum 100 ms. */
def backoffExhaustivityAnalysisTimeout(): Unit =
myExhaustivityAnalysisTimeout =
(exhaustivityAnalysisTimeout / 2)
.max(100)

/** Recover slowly, by 1.5 times, up to the original value. */
def recoverExhaustivityAnalysisTimeout(): Unit =
myExhaustivityAnalysisTimeout =
(exhaustivityAnalysisTimeout * 1.5).toInt
.min(ictx.settings.XpatmatAnalysisTimeout.value)

private var compiling = false

private var myUnits: List[CompilationUnit] = Nil
Expand Down
1 change: 1 addition & 0 deletions compiler/src/dotty/tools/dotc/config/ScalaSettings.scala
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,7 @@ private sealed trait XSettings:
val XverifySignatures: Setting[Boolean] = BooleanSetting(AdvancedSetting, "Xverify-signatures", "Verify generic signatures in generated bytecode.")
val XignoreScala2Macros: Setting[Boolean] = BooleanSetting(AdvancedSetting, "Xignore-scala2-macros", "Ignore errors when compiling code that calls Scala2 macros, these will fail at runtime.")
val XimportSuggestionTimeout: Setting[Int] = IntSetting(AdvancedSetting, "Ximport-suggestion-timeout", "Timeout (in ms) for searching for import suggestions when errors are reported.", 8000)
val XpatmatAnalysisTimeout: Setting[Int] = IntSetting(AdvancedSetting, "Xpatmat-analysis-timeout", "Timeout (in ms) for match analysis.", 8 * 1000) // 8s
val Xsemanticdb: Setting[Boolean] = BooleanSetting(AdvancedSetting, "Xsemanticdb", "Store information in SemanticDB.", aliases = List("-Ysemanticdb"))
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"))
val XcheckMacros: Setting[Boolean] = BooleanSetting(AdvancedSetting, "Xcheck-macros", "Check some invariants of macro generated code while expanding macros", aliases = List("--Xcheck-macros"))
Expand Down
2 changes: 2 additions & 0 deletions compiler/src/dotty/tools/dotc/printing/Formatting.scala
Original file line number Diff line number Diff line change
Expand Up @@ -115,13 +115,15 @@ object Formatting {
given Show[Char] = ShowAny
given Show[Boolean] = ShowAny
given Show[Integer] = ShowAny
given Show[Long] = ShowAny
given Show[String] = ShowAny
given Show[Class[?]] = ShowAny
given Show[Throwable] = ShowAny
given Show[StringBuffer] = ShowAny
given Show[CompilationUnit] = ShowAny
given Show[Phases.Phase] = ShowAny
given Show[TyperState] = ShowAny
given Show[Unit] = ShowAny
given Show[config.ScalaVersion] = ShowAny
given Show[io.AbstractFile] = ShowAny
given Show[parsing.Scanners.Scanner] = ShowAny
Expand Down
9 changes: 8 additions & 1 deletion compiler/src/dotty/tools/dotc/reporting/trace.scala
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ trait TraceSyntax:
(op: => T)(using Context): T =
if ctx.mode.is(Mode.Printing) || !isForced && (printer eq Printers.noPrinter) then op
else
val start = System.nanoTime
// Avoid evaluating question multiple time, since each evaluation
// may cause some extra logging output.
val q = question
Expand All @@ -109,7 +110,13 @@ trait TraceSyntax:
def finalize(msg: String) =
if !finalized then
ctx.base.indent -= 1
doLog(s"$margin$msg")
val stop = System.nanoTime
val diffNs = stop - start
val diffS = (diffNs / 1000 / 1000).toInt / 1000.0
if diffS > 0.1 then
doLog(s"$margin$msg (${"%.2f".format(diffS)} s)")
else
doLog(s"$margin$msg")
finalized = true
try
doLog(s"$margin$leading")
Expand Down
44 changes: 40 additions & 4 deletions compiler/src/dotty/tools/dotc/transform/patmat/Space.scala
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,20 @@ case class Prod(tp: Type, unappTp: TermRef, params: List[Space]) extends Space
case class Or(spaces: Seq[Space]) extends Space

object SpaceEngine {
val DeadlineKey = new Property.Key[Long]

def setupDeadline(using Context): Context =
val timeout = ctx.run.nn.exhaustivityAnalysisTimeout
if timeout >= 0 then
ctx.fresh.setProperty(DeadlineKey, System.currentTimeMillis() + timeout)
else
ctx

def isPastDeadline(using Context): Boolean =
ctx.property(DeadlineKey) match
case Some(deadline) => System.currentTimeMillis() > deadline
case _ => false

def simplify(space: Space)(using Context): Space = space.simplify
def isSubspace(a: Space, b: Space)(using Context): Boolean = a.isSubspace(b)
def canDecompose(typ: Typ)(using Context): Boolean = typ.canDecompose
Expand Down Expand Up @@ -590,6 +604,7 @@ object SpaceEngine {

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

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

val parts = children.map { sym =>
val sym1 = if (sym.is(ModuleClass)) sym.sourceModule else sym
val refined = trace(i"refineUsingParent($tp, $sym1, $mixins)")(TypeOps.refineUsingParent(tp, sym1, mixins))
val refined =
if isPastDeadline then NoType
else TypeOps.refineUsingParent(tp, sym1, mixins)

def inhabited(tp: Type): Boolean = tp.dealias match
case AndType(tp1, tp2) => !TypeComparer.provablyDisjoint(tp1, tp2)
case OrType(tp1, tp2) => inhabited(tp1) || inhabited(tp2)
case tp: RefinedType => inhabited(tp.parent)
case tp: TypeRef => inhabited(tp.prefix)
case NoType => false
case _ => true

if inhabited(refined) then refined
Expand Down Expand Up @@ -862,6 +881,21 @@ object SpaceEngine {
case _ => tp
})

def checkExhaustivityInDeadline(m: Match)(using Context): Unit = {
inContext(setupDeadline):
checkExhaustivity(m)
if isPastDeadline then
ctx.run.nn.backoffExhaustivityAnalysisTimeout()
val setting = ctx.settings.XpatmatAnalysisTimeout
report.warning(
em"""Match analysis requires more time than allowed. You can try:
| * doubling the timeout: ${setting.name}:${setting.value * 2}
| * disabling the timeout: ${setting.name}:-1
| * adding `: @unchecked` to the scrutinee""", m.srcPos)
else
ctx.run.nn.recoverExhaustivityAnalysisTimeout()
}

def checkExhaustivity(m: Match)(using Context): Unit = trace(i"checkExhaustivity($m)") {
val selTyp = toUnderlying(m.selector.tpe.stripUnsafeNulls()).dealias
val targetSpace = trace(i"targetSpace($selTyp)")(project(selTyp))
Expand All @@ -880,7 +914,8 @@ object SpaceEngine {

if uncovered.nonEmpty then
val deduped = dedup(uncovered)
report.warning(PatternMatchExhaustivity(deduped, m), m.selector)
if !isPastDeadline then
report.warning(PatternMatchExhaustivity(deduped, m), m.selector)
}

private def reachabilityCheckable(sel: Tree)(using Context): Boolean =
Expand Down Expand Up @@ -929,7 +964,8 @@ object SpaceEngine {
then {
val nullOnly = isNullable && i == len - 1 && isWildcardArg(pat)
val msg = if nullOnly then MatchCaseOnlyNullWarning() else MatchCaseUnreachable()
report.warning(msg, pat.srcPos)
if !isPastDeadline then
report.warning(msg, pat.srcPos)
}
deferred = Nil
}
Expand All @@ -941,6 +977,6 @@ object SpaceEngine {
}

def checkMatch(m: Match)(using Context): Unit =
if exhaustivityCheckable(m.selector) then checkExhaustivity(m)
if exhaustivityCheckable(m.selector) then checkExhaustivityInDeadline(m)
if reachabilityCheckable(m.selector) then checkReachability(m)
}
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,7 @@ trait ImportSuggestions:
end importSuggestions

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

0 comments on commit de1727b

Please sign in to comment.