Skip to content

Commit 0da9afd

Browse files
authored
Merge pull request #11721 from dotty-staging/add-safe-throws-2
Safer exceptions
2 parents ba0ac5e + 9b35e0b commit 0da9afd

23 files changed

+288
-75
lines changed

compiler/src/dotty/tools/dotc/ast/Desugar.scala

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1261,6 +1261,16 @@ object desugar {
12611261
makeOp(left, right, Span(left.span.start, op.span.end, op.span.start))
12621262
}
12631263

1264+
/** Translate throws type `A throws E1 | ... | En` to
1265+
* $throws[... $throws[A, E1] ... , En].
1266+
*/
1267+
def throws(tpt: Tree, op: Ident, excepts: Tree)(using Context): AppliedTypeTree = excepts match
1268+
case InfixOp(l, bar @ Ident(tpnme.raw.BAR), r) =>
1269+
throws(throws(tpt, op, l), bar, r)
1270+
case e =>
1271+
AppliedTypeTree(
1272+
TypeTree(defn.throwsAlias.typeRef).withSpan(op.span), tpt :: excepts :: Nil)
1273+
12641274
/** Translate tuple expressions of arity <= 22
12651275
*
12661276
* () ==> ()

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ object Feature:
2727
val erasedDefinitions = experimental("erasedDefinitions")
2828
val symbolLiterals = deprecated("symbolLiterals")
2929
val fewerBraces = experimental("fewerBraces")
30+
val saferExceptions = experimental("saferExceptions")
3031

3132
/** Is `feature` enabled by by a command-line setting? The enabling setting is
3233
*

compiler/src/dotty/tools/dotc/core/Definitions.scala

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -657,8 +657,11 @@ class Definitions {
657657

658658
// in scalac modified to have Any as parent
659659

660-
@tu lazy val ThrowableType: TypeRef = requiredClassRef("java.lang.Throwable")
661-
def ThrowableClass(using Context): ClassSymbol = ThrowableType.symbol.asClass
660+
@tu lazy val ThrowableType: TypeRef = requiredClassRef("java.lang.Throwable")
661+
def ThrowableClass(using Context): ClassSymbol = ThrowableType.symbol.asClass
662+
@tu lazy val ExceptionClass: ClassSymbol = requiredClass("java.lang.Exception")
663+
@tu lazy val RuntimeExceptionClass: ClassSymbol = requiredClass("java.lang.RuntimeException")
664+
662665
@tu lazy val SerializableType: TypeRef = JavaSerializableClass.typeRef
663666
def SerializableClass(using Context): ClassSymbol = SerializableType.symbol.asClass
664667

@@ -830,6 +833,9 @@ class Definitions {
830833
val methodName = if CanEqualClass.name == tpnme.Eql then nme.eqlAny else nme.canEqualAny
831834
CanEqualClass.companionModule.requiredMethod(methodName)
832835

836+
@tu lazy val CanThrowClass: ClassSymbol = requiredClass("scala.CanThrow")
837+
@tu lazy val throwsAlias: Symbol = ScalaRuntimePackageVal.requiredType(tpnme.THROWS)
838+
833839
@tu lazy val TypeBoxClass: ClassSymbol = requiredClass("scala.runtime.TypeBox")
834840
@tu lazy val TypeBox_CAP: TypeSymbol = TypeBoxClass.requiredType(tpnme.CAP)
835841

compiler/src/dotty/tools/dotc/core/StdNames.scala

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,7 @@ object StdNames {
305305
val SPECIALIZED_INSTANCE: N = "specInstance$"
306306
val THIS: N = "_$this"
307307
val TRAIT_CONSTRUCTOR: N = "$init$"
308+
val THROWS: N = "$throws"
308309
val U2EVT: N = "u2evt$"
309310
val ALLARGS: N = "$allArgs"
310311

@@ -602,6 +603,7 @@ object StdNames {
602603
val this_ : N = "this"
603604
val thisPrefix : N = "thisPrefix"
604605
val throw_ : N = "throw"
606+
val throws: N = "throws"
605607
val toArray: N = "toArray"
606608
val toList: N = "toList"
607609
val toObjectArray : N = "toObjectArray"

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,16 @@ object TypeUtils {
2424
def isErasedClass(using Context): Boolean =
2525
self.underlyingClassRef(refinementOK = true).typeSymbol.is(Flags.Erased)
2626

27+
/** Is this type a checked exception? This is the case if the type
28+
* derives from Exception but not from RuntimeException. According to
29+
* that definition Throwable is unchecked. That makes sense since you should
30+
* neither throw nor catch `Throwable` anyway, so we should not define
31+
* a capability to do so.
32+
*/
33+
def isCheckedException(using Context): Boolean =
34+
self.derivesFrom(defn.ExceptionClass)
35+
&& !self.derivesFrom(defn.RuntimeExceptionClass)
36+
2737
def isByName: Boolean =
2838
self.isInstanceOf[ExprType]
2939

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

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,10 @@ import NameOps._
3434
import SymDenotations.{NoCompleter, NoDenotation}
3535
import Applications.unapplyArgs
3636
import transform.patmat.SpaceEngine.isIrrefutable
37-
import config.Feature._
37+
import config.Feature
38+
import config.Feature.sourceVersion
3839
import config.SourceVersion._
40+
import transform.TypeUtils.*
3941

4042
import collection.mutable
4143
import reporting._
@@ -930,7 +932,7 @@ trait Checking {
930932
description: => String,
931933
featureUseSite: Symbol,
932934
pos: SrcPos)(using Context): Unit =
933-
if !enabled(name) then
935+
if !Feature.enabled(name) then
934936
report.featureWarning(name.toString, description, featureUseSite, required = false, pos)
935937

936938
/** Check that `tp` is a class type and that any top-level type arguments in this type
@@ -1312,6 +1314,10 @@ trait Checking {
13121314
if !tp.derivesFrom(defn.MatchableClass) && sourceVersion.isAtLeast(`future-migration`) then
13131315
val kind = if pattern then "pattern selector" else "value"
13141316
report.warning(MatchableWarning(tp, pattern), pos)
1317+
1318+
def checkCanThrow(tp: Type, span: Span)(using Context): Unit =
1319+
if Feature.enabled(Feature.saferExceptions) && tp.isCheckedException then
1320+
ctx.typer.implicitArgTree(defn.CanThrowClass.typeRef.appliedTo(tp), span)
13151321
}
13161322

13171323
trait ReChecking extends Checking {
@@ -1324,6 +1330,7 @@ trait ReChecking extends Checking {
13241330
override def checkAnnotApplicable(annot: Tree, sym: Symbol)(using Context): Boolean = true
13251331
override def checkMatchable(tp: Type, pos: SrcPos, pattern: Boolean)(using Context): Unit = ()
13261332
override def checkNoModuleClash(sym: Symbol)(using Context) = ()
1333+
override def checkCanThrow(tp: Type, span: Span)(using Context): Unit = ()
13271334
}
13281335

13291336
trait NoChecking extends ReChecking {

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,9 @@ class ReTyper extends Typer with ReChecking {
114114
super.handleUnexpectedFunType(tree, fun)
115115
}
116116

117+
override def addCanThrowCapabilities(expr: untpd.Tree, cases: List[CaseDef])(using Context): untpd.Tree =
118+
expr
119+
117120
override def typedUnadapted(tree: untpd.Tree, pt: Type, locked: TypeVars)(using Context): Tree =
118121
try super.typedUnadapted(tree, pt, locked)
119122
catch {

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

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@ import annotation.tailrec
3939
import Implicits._
4040
import util.Stats.record
4141
import config.Printers.{gadts, typr, debug}
42-
import config.Feature._
42+
import config.Feature
43+
import config.Feature.{sourceVersion, migrateTo3}
4344
import config.SourceVersion._
4445
import rewrites.Rewrites.patch
4546
import NavigateAST._
@@ -712,7 +713,7 @@ class Typer extends Namer
712713
case Whole(16) => // cant parse hex literal as double
713714
case _ => return lit(doubleFromDigits(digits))
714715
}
715-
else if genericNumberLiteralsEnabled
716+
else if Feature.genericNumberLiteralsEnabled
716717
&& target.isValueType && isFullyDefined(target, ForceDegree.none)
717718
then
718719
// If expected type is defined with a FromDigits instance, use that one
@@ -1712,10 +1713,30 @@ class Typer extends Namer
17121713
.withNotNullInfo(body1.notNullInfo.retractedInfo.seq(cond1.notNullInfoIf(false)))
17131714
}
17141715

1716+
/** Add givens reflecting `CanThrow` capabilities for all checked exceptions matched
1717+
* by `cases`. The givens appear in nested blocks with earlier cases leading to
1718+
* more deeply nested givens. This way, given priority will be the same as pattern priority.
1719+
* The functionality is enabled if the experimental.saferExceptions language feature is enabled.
1720+
*/
1721+
def addCanThrowCapabilities(expr: untpd.Tree, cases: List[CaseDef])(using Context): untpd.Tree =
1722+
def makeCanThrow(tp: Type): untpd.Tree =
1723+
untpd.ValDef(
1724+
EvidenceParamName.fresh(),
1725+
untpd.TypeTree(defn.CanThrowClass.typeRef.appliedTo(tp)),
1726+
untpd.ref(defn.Predef_undefined))
1727+
.withFlags(Given | Final | Lazy | Erased)
1728+
.withSpan(expr.span)
1729+
val caps =
1730+
for
1731+
CaseDef(pat, _, _) <- cases
1732+
if Feature.enabled(Feature.saferExceptions) && pat.tpe.widen.isCheckedException
1733+
yield makeCanThrow(pat.tpe.widen)
1734+
caps.foldLeft(expr)((e, g) => untpd.Block(g :: Nil, e))
1735+
17151736
def typedTry(tree: untpd.Try, pt: Type)(using Context): Try = {
17161737
val expr2 :: cases2x = harmonic(harmonize, pt) {
1717-
val expr1 = typed(tree.expr, pt.dropIfProto)
17181738
val cases1 = typedCases(tree.cases, EmptyTree, defn.ThrowableType, pt.dropIfProto)
1739+
val expr1 = typed(addCanThrowCapabilities(tree.expr, cases1), pt.dropIfProto)
17191740
expr1 :: cases1
17201741
}
17211742
val finalizer1 = typed(tree.finalizer, defn.UnitType)
@@ -1734,6 +1755,7 @@ class Typer extends Namer
17341755

17351756
def typedThrow(tree: untpd.Throw)(using Context): Tree = {
17361757
val expr1 = typed(tree.expr, defn.ThrowableType)
1758+
checkCanThrow(expr1.tpe.widen, tree.span)
17371759
Throw(expr1).withSpan(tree.span)
17381760
}
17391761

@@ -1832,7 +1854,7 @@ class Typer extends Namer
18321854
def typedAppliedTypeTree(tree: untpd.AppliedTypeTree)(using Context): Tree = {
18331855
tree.args match
18341856
case arg :: _ if arg.isTerm =>
1835-
if dependentEnabled then
1857+
if Feature.dependentEnabled then
18361858
return errorTree(tree, i"Not yet implemented: T(...)")
18371859
else
18381860
return errorTree(tree, dependentStr)
@@ -1928,7 +1950,7 @@ class Typer extends Namer
19281950
typeIndexedLambdaTypeTree(tree, tparams, body)
19291951

19301952
def typedTermLambdaTypeTree(tree: untpd.TermLambdaTypeTree)(using Context): Tree =
1931-
if dependentEnabled then
1953+
if Feature.dependentEnabled then
19321954
errorTree(tree, i"Not yet implemented: (...) =>> ...")
19331955
else
19341956
errorTree(tree, dependentStr)
@@ -2399,7 +2421,7 @@ class Typer extends Namer
23992421
ctx.phase.isTyper &&
24002422
cdef1.symbol.ne(defn.DynamicClass) &&
24012423
cdef1.tpe.derivesFrom(defn.DynamicClass) &&
2402-
!dynamicsEnabled
2424+
!Feature.dynamicsEnabled
24032425
if (reportDynamicInheritance) {
24042426
val isRequired = parents1.exists(_.tpe.isRef(defn.DynamicClass))
24052427
report.featureWarning(nme.dynamics.toString, "extension of type scala.Dynamic", cls, isRequired, cdef.srcPos)
@@ -2614,7 +2636,10 @@ class Typer extends Namer
26142636
val untpd.InfixOp(l, op, r) = tree
26152637
val result =
26162638
if (ctx.mode.is(Mode.Type))
2617-
typedAppliedTypeTree(cpy.AppliedTypeTree(tree)(op, l :: r :: Nil))
2639+
typedAppliedTypeTree(
2640+
if op.name == tpnme.throws && Feature.enabled(Feature.saferExceptions)
2641+
then desugar.throws(l, op, r)
2642+
else cpy.AppliedTypeTree(tree)(op, l :: r :: Nil))
26182643
else if (ctx.mode.is(Mode.Pattern))
26192644
typedUnApply(cpy.Apply(tree)(op, l :: r :: Nil), pt)
26202645
else {
@@ -3468,7 +3493,7 @@ class Typer extends Namer
34683493
def isAutoApplied(sym: Symbol): Boolean =
34693494
sym.isConstructor
34703495
|| sym.matchNullaryLoosely
3471-
|| warnOnMigration(MissingEmptyArgumentList(sym.show), tree.srcPos)
3496+
|| Feature.warnOnMigration(MissingEmptyArgumentList(sym.show), tree.srcPos)
34723497
&& { patch(tree.span.endPos, "()"); true }
34733498

34743499
// Reasons NOT to eta expand:
@@ -3819,7 +3844,7 @@ class Typer extends Namer
38193844
case ref: TermRef =>
38203845
pt match {
38213846
case pt: FunProto
3822-
if needsTupledDual(ref, pt) && autoTuplingEnabled =>
3847+
if needsTupledDual(ref, pt) && Feature.autoTuplingEnabled =>
38233848
adapt(tree, pt.tupledDual, locked)
38243849
case _ =>
38253850
adaptOverloaded(ref)

0 commit comments

Comments
 (0)