Skip to content

Commit 192d387

Browse files
authored
Merge pull request #9411 from sjrd/scalajs-match
Fix #9268: Scala.js: Add support for switch-Matches.
2 parents ceccb9e + bb5a93b commit 192d387

File tree

3 files changed

+146
-14
lines changed

3 files changed

+146
-14
lines changed

compiler/src/dotty/tools/backend/sjs/JSCodeGen.scala

Lines changed: 123 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ import dotty.tools.dotc.util.Spans.Span
3030
import dotty.tools.dotc.report
3131

3232
import org.scalajs.ir
33-
import org.scalajs.ir.{ClassKind, Position, Trees => js, Types => jstpe}
33+
import org.scalajs.ir.{ClassKind, Position, Names => jsNames, Trees => js, Types => jstpe}
3434
import org.scalajs.ir.Names.{ClassName, MethodName, SimpleMethodName}
3535
import org.scalajs.ir.OriginalName
3636
import org.scalajs.ir.OriginalName.NoOriginalName
@@ -1176,9 +1176,7 @@ class JSCodeGen()(using genCtx: Context) {
11761176

11771177
/** A Match reaching the backend is supposed to be optimized as a switch */
11781178
case mtch: Match =>
1179-
// TODO Correctly handle `Match` nodes
1180-
//genMatch(mtch, isStat)
1181-
js.Throw(js.Null())
1179+
genMatch(mtch, isStat)
11821180

11831181
case tree: Closure =>
11841182
genClosure(tree)
@@ -2228,6 +2226,127 @@ class JSCodeGen()(using genCtx: Context) {
22282226
js.ArrayValue(arrayTypeRef, genElems)
22292227
}
22302228

2229+
/** Gen JS code for a switch-`Match`, which is translated into an IR `js.Match`. */
2230+
def genMatch(tree: Tree, isStat: Boolean): js.Tree = {
2231+
implicit val pos = tree.span
2232+
val Match(selector, cases) = tree
2233+
2234+
def abortMatch(msg: String): Nothing =
2235+
throw new FatalError(s"$msg in switch-like pattern match at ${tree.span}: $tree")
2236+
2237+
/* Although GenBCode adapts the scrutinee and the cases to `int`, only
2238+
* true `int`s can reach the back-end, as asserted by the String-switch
2239+
* transformation in `cleanup`. Therefore, we do not adapt, preserving
2240+
* the `string`s and `null`s that come out of the pattern matching in
2241+
* Scala 2.13.2+.
2242+
*/
2243+
val genSelector = genExpr(selector)
2244+
2245+
// Sanity check: we can handle Ints and Strings (including `null`s), but nothing else
2246+
genSelector.tpe match {
2247+
case jstpe.IntType | jstpe.ClassType(jsNames.BoxedStringClass) | jstpe.NullType | jstpe.NothingType =>
2248+
// ok
2249+
case _ =>
2250+
abortMatch(s"Invalid selector type ${genSelector.tpe}")
2251+
}
2252+
2253+
val resultType =
2254+
if (isStat) jstpe.NoType
2255+
else toIRType(tree.tpe)
2256+
2257+
var clauses: List[(List[js.Tree], js.Tree)] = Nil
2258+
var optDefaultClause: Option[js.Tree] = None
2259+
2260+
for (caze @ CaseDef(pat, guard, body) <- cases) {
2261+
if (guard != EmptyTree)
2262+
abortMatch("Found a case guard")
2263+
2264+
val genBody = genStatOrExpr(body, isStat)
2265+
2266+
pat match {
2267+
case lit: Literal =>
2268+
clauses = (List(genExpr(lit)), genBody) :: clauses
2269+
case Ident(nme.WILDCARD) =>
2270+
optDefaultClause = Some(genBody)
2271+
case Alternative(alts) =>
2272+
val genAlts = alts.map {
2273+
case lit: Literal => genExpr(lit)
2274+
case _ => abortMatch("Invalid case in alternative")
2275+
}
2276+
clauses = (genAlts, genBody) :: clauses
2277+
case _ =>
2278+
abortMatch("Invalid case pattern")
2279+
}
2280+
}
2281+
2282+
clauses = clauses.reverse
2283+
val defaultClause = optDefaultClause.getOrElse {
2284+
throw new AssertionError("No elseClause in pattern match")
2285+
}
2286+
2287+
/* Builds a `js.Match`, but simplifies it to a `js.If` if there is only
2288+
* one case with one alternative, and to a `js.Block` if there is no case
2289+
* at all. This happens in practice in the standard library. Having no
2290+
* case is a typical product of `match`es that are full of
2291+
* `case n if ... =>`, which are used instead of `if` chains for
2292+
* convenience and/or readability.
2293+
*
2294+
* When no optimization applies, and any of the case values is not a
2295+
* literal int, we emit a series of `if..else` instead of a `js.Match`.
2296+
* This became necessary in 2.13.2 with strings and nulls.
2297+
*
2298+
* Note that dotc has not adopted String-switch-Matches yet, so these code
2299+
* paths are dead code at the moment. However, they already existed in the
2300+
* scalac, so were ported, to be immediately available and working when
2301+
* dotc starts emitting switch-Matches on Strings.
2302+
*/
2303+
def isInt(tree: js.Tree): Boolean = tree.tpe == jstpe.IntType
2304+
2305+
clauses match {
2306+
case Nil =>
2307+
// Completely remove the Match. Preserve the side-effects of `genSelector`.
2308+
js.Block(exprToStat(genSelector), defaultClause)
2309+
2310+
case (uniqueAlt :: Nil, caseRhs) :: Nil =>
2311+
/* Simplify the `match` as an `if`, so that the optimizer has less
2312+
* work to do, and we emit less code at the end of the day.
2313+
* Use `Int_==` instead of `===` if possible, since it is a common case.
2314+
*/
2315+
val op =
2316+
if (isInt(genSelector) && isInt(uniqueAlt)) js.BinaryOp.Int_==
2317+
else js.BinaryOp.===
2318+
js.If(js.BinaryOp(op, genSelector, uniqueAlt), caseRhs, defaultClause)(resultType)
2319+
2320+
case _ =>
2321+
if (isInt(genSelector) &&
2322+
clauses.forall(_._1.forall(_.isInstanceOf[js.IntLiteral]))) {
2323+
// We have int literals only: use a js.Match
2324+
val intClauses = clauses.asInstanceOf[List[(List[js.IntLiteral], js.Tree)]]
2325+
js.Match(genSelector, intClauses, defaultClause)(resultType)
2326+
} else {
2327+
// We have other stuff: generate an if..else chain
2328+
val (tempSelectorDef, tempSelectorRef) = genSelector match {
2329+
case varRef: js.VarRef =>
2330+
(js.Skip(), varRef)
2331+
case _ =>
2332+
val varDef = js.VarDef(freshLocalIdent(), NoOriginalName,
2333+
genSelector.tpe, mutable = false, genSelector)
2334+
(varDef, varDef.ref)
2335+
}
2336+
val ifElseChain = clauses.foldRight(defaultClause) { (caze, elsep) =>
2337+
val conds = caze._1.map { caseValue =>
2338+
js.BinaryOp(js.BinaryOp.===, tempSelectorRef, caseValue)
2339+
}
2340+
val cond = conds.reduceRight[js.Tree] { (left, right) =>
2341+
js.If(left, js.BooleanLiteral(true), right)(jstpe.BooleanType)
2342+
}
2343+
js.If(cond, caze._2, elsep)(resultType)
2344+
}
2345+
js.Block(tempSelectorDef, ifElseChain)
2346+
}
2347+
}
2348+
}
2349+
22312350
/** Gen JS code for a closure.
22322351
*
22332352
* Input: a `Closure` tree of the form

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

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -821,17 +821,31 @@ object PatternMatcher {
821821
}
822822
}
823823

824-
/** Emit cases of a switch */
825-
private def emitSwitchCases(cases: List[(List[Tree], Plan)]): List[CaseDef] = (cases: @unchecked) match {
826-
case (alts, ons) :: cases1 =>
824+
/** Emit a switch-match */
825+
private def emitSwitchMatch(scrutinee: Tree, cases: List[(List[Tree], Plan)]): Match = {
826+
/* Make sure to adapt the scrutinee to Int, as well as all the alternatives
827+
* of all cases, so that only Matches on pritimive Ints survive this phase.
828+
*/
829+
830+
val intScrutinee =
831+
if (scrutinee.tpe.widen.isRef(defn.IntClass)) scrutinee
832+
else scrutinee.select(nme.toInt)
833+
834+
def intLiteral(lit: Tree): Tree =
835+
val Literal(constant) = lit
836+
if (constant.tag == Constants.IntTag) lit
837+
else cpy.Literal(lit)(Constant(constant.intValue))
838+
839+
val caseDefs = cases.map { (alts, ons) =>
827840
val pat = alts match {
828-
case alt :: Nil => alt
841+
case alt :: Nil => intLiteral(alt)
829842
case Nil => Underscore(defn.IntType) // default case
830-
case _ => Alternative(alts)
843+
case _ => Alternative(alts.map(intLiteral))
831844
}
832-
CaseDef(pat, EmptyTree, emit(ons)) :: emitSwitchCases(cases1)
833-
case nil =>
834-
Nil
845+
CaseDef(pat, EmptyTree, emit(ons))
846+
}
847+
848+
Match(intScrutinee, caseDefs)
835849
}
836850

837851
/** If selfCheck is `true`, used to check whether a tree gets generated twice */
@@ -892,7 +906,7 @@ object PatternMatcher {
892906
def maybeEmitSwitch(scrutinee: Tree): Tree = {
893907
val switchCases = collectSwitchCases(scrutinee, plan)
894908
if (hasEnoughSwitchCases(switchCases, MinSwitchCases)) // at least 3 cases + default
895-
Match(scrutinee, emitSwitchCases(switchCases))
909+
emitSwitchMatch(scrutinee, switchCases)
896910
else
897911
default
898912
}

project/Build.scala

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1047,7 +1047,6 @@ object Build {
10471047
-- "CollectionsOnSynchronizedSetTest.scala"
10481048
-- "CollectionsTest.scala"
10491049
-- "EventObjectTest.scala"
1050-
-- "FormatterTest.scala"
10511050
-- "HashSetTest.scala"
10521051
-- "LinkedHashSetTest.scala"
10531052
-- "SortedSetTest.scala"

0 commit comments

Comments
 (0)