Skip to content

Commit f38921f

Browse files
committed
Fix #854: Optimize matches on primitive constants as switches.
This does not yet unable the checks that `@switch` verifies that the compiler was indeed able to perform the optimization. This implementation also does not support guards. A match with guards will never be optimized as a switch.
1 parent 2fe8ad5 commit f38921f

File tree

1 file changed

+133
-2
lines changed

1 file changed

+133
-2
lines changed

src/dotty/tools/dotc/transform/PatternMatcher.scala

+133-2
Original file line numberDiff line numberDiff line change
@@ -303,8 +303,139 @@ class PatternMatcher extends MiniPhaseTransform with DenotTransformer {thisTrans
303303
def optimizeCases(prevBinder: Symbol, cases: List[List[TreeMaker]], pt: Type): (List[List[TreeMaker]], List[Tree])
304304
def analyzeCases(prevBinder: Symbol, cases: List[List[TreeMaker]], pt: Type, suppression: Suppression): Unit = {}
305305

306-
def emitSwitch(scrut: Tree, scrutSym: Symbol, cases: List[List[TreeMaker]], pt: Type, matchFailGenOverride: Option[Symbol => Tree], unchecked: Boolean): Option[Tree] =
307-
None // todo
306+
def emitSwitch(scrut: Tree, scrutSym: Symbol, cases: List[List[TreeMaker]], pt: Type, matchFailGenOverride: Option[Symbol => Tree], unchecked: Boolean): Option[Tree] = {
307+
// TODO Deal with guards?
308+
309+
def isSwitchableType(tpe: Type): Boolean = {
310+
(tpe isRef defn.IntClass) ||
311+
(tpe isRef defn.ByteClass) ||
312+
(tpe isRef defn.ShortClass) ||
313+
(tpe isRef defn.CharClass)
314+
}
315+
316+
object IntEqualityTestTreeMaker {
317+
def unapply(treeMaker: EqualityTestTreeMaker): Option[Int] = treeMaker match {
318+
case EqualityTestTreeMaker(`scrutSym`, _, Literal(const), _) =>
319+
if (const.isIntRange) Some(const.intValue)
320+
else None
321+
case _ =>
322+
None
323+
}
324+
}
325+
326+
def isSwitchCase(treeMakers: List[TreeMaker]): Boolean = treeMakers match {
327+
// case 5 =>
328+
case List(IntEqualityTestTreeMaker(_), _: BodyTreeMaker) =>
329+
true
330+
331+
// case 5 | 6 =>
332+
case List(AlternativesTreeMaker(`scrutSym`, alts, _), _: BodyTreeMaker) =>
333+
alts.forall {
334+
case List(IntEqualityTestTreeMaker(_)) => true
335+
case _ => false
336+
}
337+
338+
// case _ =>
339+
case List(_: BodyTreeMaker) =>
340+
true
341+
342+
/* case x @ pat =>
343+
* This includes:
344+
* case x =>
345+
* case x @ 5 =>
346+
* case x @ (5 | 6) =>
347+
*/
348+
case (_: SubstOnlyTreeMaker) :: rest =>
349+
isSwitchCase(rest)
350+
351+
case _ =>
352+
false
353+
}
354+
355+
/* (Nil, body) means that `body` is the default case
356+
* It's a bit hacky but it simplifies manipulations.
357+
*/
358+
def extractSwitchCase(treeMakers: List[TreeMaker]): (List[Int], BodyTreeMaker) = treeMakers match {
359+
// case 5 =>
360+
case List(IntEqualityTestTreeMaker(intValue), body: BodyTreeMaker) =>
361+
(List(intValue), body)
362+
363+
// case 5 | 6 =>
364+
case List(AlternativesTreeMaker(_, alts, _), body: BodyTreeMaker) =>
365+
val intValues = alts.map {
366+
case List(IntEqualityTestTreeMaker(intValue)) => intValue
367+
}
368+
(intValues, body)
369+
370+
// case _ =>
371+
case List(body: BodyTreeMaker) =>
372+
(Nil, body)
373+
374+
// case x @ pat =>
375+
case (_: SubstOnlyTreeMaker) :: rest =>
376+
/* Rebindings have been propagated, so the eventual body in `rest`
377+
* contains all the necessary information. The substitution can be
378+
* dropped at this point.
379+
*/
380+
extractSwitchCase(rest)
381+
}
382+
383+
def doOverlap(a: List[Int], b: List[Int]): Boolean =
384+
a.exists(b.contains _)
385+
386+
def makeSwitch(valuesToCases: List[(List[Int], BodyTreeMaker)]): Tree = {
387+
def genBody(body: BodyTreeMaker): Tree = {
388+
val valDefs = body.rebindings.emitValDefs
389+
if (valDefs.isEmpty) body.body
390+
else Block(valDefs, body.body)
391+
}
392+
393+
val intScrut =
394+
if (pt isRef defn.IntClass) ref(scrutSym)
395+
else Select(ref(scrutSym), nme.toInt)
396+
397+
val (normalCases, defaultCaseAndRest) = valuesToCases.span(_._1.nonEmpty)
398+
399+
val newCases = for {
400+
(values, body) <- normalCases
401+
} yield {
402+
val literals = values.map(v => Literal(Constant(v)))
403+
val pat =
404+
if (literals.size == 1) literals.head
405+
else Alternative(literals)
406+
CaseDef(pat, EmptyTree, genBody(body))
407+
}
408+
409+
val catchAllDef = {
410+
if (defaultCaseAndRest.isEmpty) {
411+
matchFailGenOverride.fold[Tree](
412+
Throw(New(defn.MatchErrorType, List(ref(scrutSym)))))(
413+
_(scrutSym))
414+
} else {
415+
/* After the default case, assuming the IR even allows anything,
416+
* things are unreachable anyway and can be removed.
417+
*/
418+
genBody(defaultCaseAndRest.head._2)
419+
}
420+
}
421+
val defaultCase = CaseDef(Underscore(defn.IntType), EmptyTree, catchAllDef)
422+
423+
Match(intScrut, newCases :+ defaultCase)
424+
}
425+
426+
if (isSwitchableType(scrut.tpe.widenDealias) && cases.forall(isSwitchCase)) {
427+
val valuesToCases = cases.map(extractSwitchCase)
428+
val values = valuesToCases.map(_._1)
429+
if (values.tails.exists { tail => tail.nonEmpty && tail.tail.exists(doOverlap(_, tail.head)) }) {
430+
// TODO Deal with overlapping cases (mostly useless without guards)
431+
None
432+
} else {
433+
Some(makeSwitch(valuesToCases))
434+
}
435+
} else {
436+
None
437+
}
438+
}
308439

309440
// for catch (no need to customize match failure)
310441
def emitTypeSwitch(bindersAndCases: List[(Symbol, List[TreeMaker])], pt: Type): Option[List[CaseDef]] =

0 commit comments

Comments
 (0)