Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Safer exceptions #11721

Merged
merged 6 commits into from
Aug 30, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 =
odersky marked this conversation as resolved.
Show resolved Hide resolved
untpd.ValDef(
EvidenceParamName.fresh(),
untpd.TypeTree(defn.CanThrowClass.typeRef.appliedTo(tp)),
untpd.ref(defn.Predef_undefined))
odersky marked this conversation as resolved.
Show resolved Hide resolved
.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