Skip to content

Commit

Permalink
Merge pull request #11721 from dotty-staging/add-safe-throws-2
Browse files Browse the repository at this point in the history
Safer exceptions
  • Loading branch information
odersky authored Aug 30, 2021
2 parents ba0ac5e + 9b35e0b commit 0da9afd
Show file tree
Hide file tree
Showing 23 changed files with 288 additions and 75 deletions.
10 changes: 10 additions & 0 deletions compiler/src/dotty/tools/dotc/ast/Desugar.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1261,6 +1261,16 @@ object desugar {
makeOp(left, right, Span(left.span.start, op.span.end, op.span.start))
}

/** Translate throws type `A throws E1 | ... | En` to
* $throws[... $throws[A, E1] ... , En].
*/
def throws(tpt: Tree, op: Ident, excepts: Tree)(using Context): AppliedTypeTree = excepts match
case InfixOp(l, bar @ Ident(tpnme.raw.BAR), r) =>
throws(throws(tpt, op, l), bar, r)
case e =>
AppliedTypeTree(
TypeTree(defn.throwsAlias.typeRef).withSpan(op.span), tpt :: excepts :: Nil)

/** Translate tuple expressions of arity <= 22
*
* () ==> ()
Expand Down
1 change: 1 addition & 0 deletions compiler/src/dotty/tools/dotc/config/Feature.scala
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ object Feature:
val erasedDefinitions = experimental("erasedDefinitions")
val symbolLiterals = deprecated("symbolLiterals")
val fewerBraces = experimental("fewerBraces")
val saferExceptions = experimental("saferExceptions")

/** Is `feature` enabled by by a command-line setting? The enabling setting is
*
Expand Down
10 changes: 8 additions & 2 deletions compiler/src/dotty/tools/dotc/core/Definitions.scala
Original file line number Diff line number Diff line change
Expand Up @@ -657,8 +657,11 @@ class Definitions {

// in scalac modified to have Any as parent

@tu lazy val ThrowableType: TypeRef = requiredClassRef("java.lang.Throwable")
def ThrowableClass(using Context): ClassSymbol = ThrowableType.symbol.asClass
@tu lazy val ThrowableType: TypeRef = requiredClassRef("java.lang.Throwable")
def ThrowableClass(using Context): ClassSymbol = ThrowableType.symbol.asClass
@tu lazy val ExceptionClass: ClassSymbol = requiredClass("java.lang.Exception")
@tu lazy val RuntimeExceptionClass: ClassSymbol = requiredClass("java.lang.RuntimeException")

@tu lazy val SerializableType: TypeRef = JavaSerializableClass.typeRef
def SerializableClass(using Context): ClassSymbol = SerializableType.symbol.asClass

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

@tu lazy val CanThrowClass: ClassSymbol = requiredClass("scala.CanThrow")
@tu lazy val throwsAlias: Symbol = ScalaRuntimePackageVal.requiredType(tpnme.THROWS)

@tu lazy val TypeBoxClass: ClassSymbol = requiredClass("scala.runtime.TypeBox")
@tu lazy val TypeBox_CAP: TypeSymbol = TypeBoxClass.requiredType(tpnme.CAP)

Expand Down
2 changes: 2 additions & 0 deletions compiler/src/dotty/tools/dotc/core/StdNames.scala
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,7 @@ object StdNames {
val SPECIALIZED_INSTANCE: N = "specInstance$"
val THIS: N = "_$this"
val TRAIT_CONSTRUCTOR: N = "$init$"
val THROWS: N = "$throws"
val U2EVT: N = "u2evt$"
val ALLARGS: N = "$allArgs"

Expand Down Expand Up @@ -602,6 +603,7 @@ object StdNames {
val this_ : N = "this"
val thisPrefix : N = "thisPrefix"
val throw_ : N = "throw"
val throws: N = "throws"
val toArray: N = "toArray"
val toList: N = "toList"
val toObjectArray : N = "toObjectArray"
Expand Down
10 changes: 10 additions & 0 deletions compiler/src/dotty/tools/dotc/transform/TypeUtils.scala
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,16 @@ object TypeUtils {
def isErasedClass(using Context): Boolean =
self.underlyingClassRef(refinementOK = true).typeSymbol.is(Flags.Erased)

/** Is this type a checked exception? This is the case if the type
* derives from Exception but not from RuntimeException. According to
* that definition Throwable is unchecked. That makes sense since you should
* neither throw nor catch `Throwable` anyway, so we should not define
* a capability to do so.
*/
def isCheckedException(using Context): Boolean =
self.derivesFrom(defn.ExceptionClass)
&& !self.derivesFrom(defn.RuntimeExceptionClass)

def isByName: Boolean =
self.isInstanceOf[ExprType]

Expand Down
11 changes: 9 additions & 2 deletions compiler/src/dotty/tools/dotc/typer/Checking.scala
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,10 @@ import NameOps._
import SymDenotations.{NoCompleter, NoDenotation}
import Applications.unapplyArgs
import transform.patmat.SpaceEngine.isIrrefutable
import config.Feature._
import config.Feature
import config.Feature.sourceVersion
import config.SourceVersion._
import transform.TypeUtils.*

import collection.mutable
import reporting._
Expand Down Expand Up @@ -930,7 +932,7 @@ trait Checking {
description: => String,
featureUseSite: Symbol,
pos: SrcPos)(using Context): Unit =
if !enabled(name) then
if !Feature.enabled(name) then
report.featureWarning(name.toString, description, featureUseSite, required = false, pos)

/** Check that `tp` is a class type and that any top-level type arguments in this type
Expand Down Expand Up @@ -1312,6 +1314,10 @@ trait Checking {
if !tp.derivesFrom(defn.MatchableClass) && sourceVersion.isAtLeast(`future-migration`) then
val kind = if pattern then "pattern selector" else "value"
report.warning(MatchableWarning(tp, pattern), pos)

def checkCanThrow(tp: Type, span: Span)(using Context): Unit =
if Feature.enabled(Feature.saferExceptions) && tp.isCheckedException then
ctx.typer.implicitArgTree(defn.CanThrowClass.typeRef.appliedTo(tp), span)
}

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

trait NoChecking extends ReChecking {
Expand Down
3 changes: 3 additions & 0 deletions compiler/src/dotty/tools/dotc/typer/ReTyper.scala
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,9 @@ class ReTyper extends Typer with ReChecking {
super.handleUnexpectedFunType(tree, fun)
}

override def addCanThrowCapabilities(expr: untpd.Tree, cases: List[CaseDef])(using Context): untpd.Tree =
expr

override def typedUnadapted(tree: untpd.Tree, pt: Type, locked: TypeVars)(using Context): Tree =
try super.typedUnadapted(tree, pt, locked)
catch {
Expand Down
43 changes: 34 additions & 9 deletions compiler/src/dotty/tools/dotc/typer/Typer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ import annotation.tailrec
import Implicits._
import util.Stats.record
import config.Printers.{gadts, typr, debug}
import config.Feature._
import config.Feature
import config.Feature.{sourceVersion, migrateTo3}
import config.SourceVersion._
import rewrites.Rewrites.patch
import NavigateAST._
Expand Down Expand Up @@ -712,7 +713,7 @@ class Typer extends Namer
case Whole(16) => // cant parse hex literal as double
case _ => return lit(doubleFromDigits(digits))
}
else if genericNumberLiteralsEnabled
else if Feature.genericNumberLiteralsEnabled
&& target.isValueType && isFullyDefined(target, ForceDegree.none)
then
// If expected type is defined with a FromDigits instance, use that one
Expand Down Expand Up @@ -1712,10 +1713,30 @@ class Typer extends Namer
.withNotNullInfo(body1.notNullInfo.retractedInfo.seq(cond1.notNullInfoIf(false)))
}

/** Add givens reflecting `CanThrow` capabilities for all checked exceptions matched
* by `cases`. The givens appear in nested blocks with earlier cases leading to
* more deeply nested givens. This way, given priority will be the same as pattern priority.
* The functionality is enabled if the experimental.saferExceptions language feature is enabled.
*/
def addCanThrowCapabilities(expr: untpd.Tree, cases: List[CaseDef])(using Context): untpd.Tree =
def makeCanThrow(tp: Type): untpd.Tree =
untpd.ValDef(
EvidenceParamName.fresh(),
untpd.TypeTree(defn.CanThrowClass.typeRef.appliedTo(tp)),
untpd.ref(defn.Predef_undefined))
.withFlags(Given | Final | Lazy | Erased)
.withSpan(expr.span)
val caps =
for
CaseDef(pat, _, _) <- cases
if Feature.enabled(Feature.saferExceptions) && pat.tpe.widen.isCheckedException
yield makeCanThrow(pat.tpe.widen)
caps.foldLeft(expr)((e, g) => untpd.Block(g :: Nil, e))

def typedTry(tree: untpd.Try, pt: Type)(using Context): Try = {
val expr2 :: cases2x = harmonic(harmonize, pt) {
val expr1 = typed(tree.expr, pt.dropIfProto)
val cases1 = typedCases(tree.cases, EmptyTree, defn.ThrowableType, pt.dropIfProto)
val expr1 = typed(addCanThrowCapabilities(tree.expr, cases1), pt.dropIfProto)
expr1 :: cases1
}
val finalizer1 = typed(tree.finalizer, defn.UnitType)
Expand All @@ -1734,6 +1755,7 @@ class Typer extends Namer

def typedThrow(tree: untpd.Throw)(using Context): Tree = {
val expr1 = typed(tree.expr, defn.ThrowableType)
checkCanThrow(expr1.tpe.widen, tree.span)
Throw(expr1).withSpan(tree.span)
}

Expand Down Expand Up @@ -1832,7 +1854,7 @@ class Typer extends Namer
def typedAppliedTypeTree(tree: untpd.AppliedTypeTree)(using Context): Tree = {
tree.args match
case arg :: _ if arg.isTerm =>
if dependentEnabled then
if Feature.dependentEnabled then
return errorTree(tree, i"Not yet implemented: T(...)")
else
return errorTree(tree, dependentStr)
Expand Down Expand Up @@ -1928,7 +1950,7 @@ class Typer extends Namer
typeIndexedLambdaTypeTree(tree, tparams, body)

def typedTermLambdaTypeTree(tree: untpd.TermLambdaTypeTree)(using Context): Tree =
if dependentEnabled then
if Feature.dependentEnabled then
errorTree(tree, i"Not yet implemented: (...) =>> ...")
else
errorTree(tree, dependentStr)
Expand Down Expand Up @@ -2399,7 +2421,7 @@ class Typer extends Namer
ctx.phase.isTyper &&
cdef1.symbol.ne(defn.DynamicClass) &&
cdef1.tpe.derivesFrom(defn.DynamicClass) &&
!dynamicsEnabled
!Feature.dynamicsEnabled
if (reportDynamicInheritance) {
val isRequired = parents1.exists(_.tpe.isRef(defn.DynamicClass))
report.featureWarning(nme.dynamics.toString, "extension of type scala.Dynamic", cls, isRequired, cdef.srcPos)
Expand Down Expand Up @@ -2614,7 +2636,10 @@ class Typer extends Namer
val untpd.InfixOp(l, op, r) = tree
val result =
if (ctx.mode.is(Mode.Type))
typedAppliedTypeTree(cpy.AppliedTypeTree(tree)(op, l :: r :: Nil))
typedAppliedTypeTree(
if op.name == tpnme.throws && Feature.enabled(Feature.saferExceptions)
then desugar.throws(l, op, r)
else cpy.AppliedTypeTree(tree)(op, l :: r :: Nil))
else if (ctx.mode.is(Mode.Pattern))
typedUnApply(cpy.Apply(tree)(op, l :: r :: Nil), pt)
else {
Expand Down Expand Up @@ -3468,7 +3493,7 @@ class Typer extends Namer
def isAutoApplied(sym: Symbol): Boolean =
sym.isConstructor
|| sym.matchNullaryLoosely
|| warnOnMigration(MissingEmptyArgumentList(sym.show), tree.srcPos)
|| Feature.warnOnMigration(MissingEmptyArgumentList(sym.show), tree.srcPos)
&& { patch(tree.span.endPos, "()"); true }

// Reasons NOT to eta expand:
Expand Down Expand Up @@ -3819,7 +3844,7 @@ class Typer extends Namer
case ref: TermRef =>
pt match {
case pt: FunProto
if needsTupledDual(ref, pt) && autoTuplingEnabled =>
if needsTupledDual(ref, pt) && Feature.autoTuplingEnabled =>
adapt(tree, pt.tupledDual, locked)
case _ =>
adaptOverloaded(ref)
Expand Down
Loading

0 comments on commit 0da9afd

Please sign in to comment.