Skip to content

Safer Exceptions for Scala 3 #16626

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

Draft
wants to merge 27 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
0c26195
Add specific Printer for safer-exceptions
hamzaremmal Nov 20, 2022
7b30d5c
Implement parser for throws declaration - Only methods for now
hamzaremmal Nov 20, 2022
bc72959
Desugar a throws declaration as a contextual function
hamzaremmal Nov 26, 2022
4291085
First draft of the safer-exceptions tests
hamzaremmal Nov 26, 2022
dcf75b5
Implement a warning when a Java method is called in a context where s…
hamzaremmal Nov 26, 2022
bb611c3
Add legacy code related to safer-exceptions.
hamzaremmal Nov 26, 2022
8c509f3
Use the correct Or name in Desugar.scala
hamzaremmal Nov 26, 2022
079c7b1
Implement the checks for CanThrow capabilities in Applications.scala
hamzaremmal Dec 13, 2022
6fda15f
Split union types when checking for capabilities.
hamzaremmal Dec 13, 2022
9956d5b
Correct StringInterpolator in warning message related to saferexceptions
hamzaremmal Dec 13, 2022
19cc4b4
Add call to saferExceptions printer for debugging purposes.
hamzaremmal Dec 13, 2022
9bc67cd
Clean code in Typer.scala
hamzaremmal Dec 13, 2022
a17efae
Add missing throws annotation in JavaParsers.scala
hamzaremmal Dec 13, 2022
fd8eafb
Fix some of the printer calls for saferExceptions
hamzaremmal Dec 13, 2022
3b7bc88
First draft of splitting CanThrow implicit search
hamzaremmal Jan 2, 2023
bb2570e
Add OrType::split to split recursively an OrType
hamzaremmal Jan 4, 2023
e4a769b
Implement semantics of implicit search for a CanThrow capability - st…
hamzaremmal Jan 4, 2023
4d43d99
Add check for parameterless function calls - implicit search for CanT…
hamzaremmal Jan 4, 2023
995757d
Add an error message when parsing throws clauses without enabling the…
hamzaremmal Jan 4, 2023
9a5c8ff
Change the semantics of the $throws infix type to follow new implemen…
hamzaremmal Jan 4, 2023
db484bb
Change implicitNotFound message for the CanThrow class to drop the co…
hamzaremmal Jan 4, 2023
280f564
Merge branch 'main' into safer-exceptions
hamzaremmal Jan 7, 2023
073206e
Fix error messages in Parsers & Applications
hamzaremmal Jan 19, 2023
82c0ad4
Merge remote-tracking branch 'origin/safer-exceptions' into safer-exc…
hamzaremmal Jan 19, 2023
6da1b43
Resolve type inference problem in Implicits
hamzaremmal Jan 19, 2023
36b9aa5
Restructure resolveCanThrow method to compile with scala 3.3.0
hamzaremmal Jan 19, 2023
f2f330a
Add error message when using throws clauses without return type
hamzaremmal Jan 19, 2023
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
21 changes: 18 additions & 3 deletions compiler/src/dotty/tools/dotc/ast/Desugar.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1331,13 +1331,11 @@ object desugar {
}

/** Translate throws type `A throws E1 | ... | En` to
* $throws[... $throws[A, E1] ... , En].
* $throws[A, E1| ... | En].
*/
def throws(tpt: Tree, op: Ident, excepts: Tree)(using Context): AppliedTypeTree = excepts match
case Parens(excepts1) =>
throws(tpt, op, excepts1)
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)
Expand Down Expand Up @@ -1778,6 +1776,23 @@ object desugar {
}

val desugared = tree match {
case ThrowsReturn(exceptions, rteTpe) =>
rteTpe match
case _ : TypeTree =>
report.error(
i"""
| Cannot infer return type of a definition using a throws clause.
| Not implemented yet.
|""".stripMargin,
tree.srcPos
)
rteTpe
case _ =>
val r = exceptions.reduce((left, right) => InfixOp(left, Ident(tpnme.OR), right))
val args = AppliedTypeTree(Ident(defn.CanThrowClass.name), r)
val fn = FunctionWithMods(args :: Nil, rteTpe, Modifiers(Flags.Given))
Printers.saferExceptions.println(i"throws clause desugared to $fn")
fn
case PolyFunction(targs, body) =>
makePolyFunction(targs, body, pt) orElse tree
case SymbolLit(str) =>
Expand Down
27 changes: 21 additions & 6 deletions compiler/src/dotty/tools/dotc/ast/Trees.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,34 @@ package dotty.tools
package dotc
package ast

import core._
import Types._, Names._, NameOps._, Flags._, util.Spans._, Contexts._, Constants._
import typer.{ ConstFold, ProtoTypes }
import SymDenotations._, Symbols._, Denotations._, StdNames._, Comments._
import core.*
import Types.*
import Names.*
import NameOps.*
import Flags.*
import util.Spans.*
import Contexts.*
import Constants.*
import typer.{ConstFold, ProtoTypes}
import SymDenotations.*
import Symbols.*
import Denotations.*
import StdNames.*
import Comments.*

import collection.mutable.ListBuffer
import printing.Printer
import printing.Texts.Text
import util.{Stats, Attachment, Property, SourceFile, NoSource, SrcPos, SourcePosition}
import util.{Attachment, NoSource, Property, SourceFile, SourcePosition, SrcPos, Stats}
import config.Config
import config.Printers.overload

import annotation.internal.sharable
import annotation.unchecked.uncheckedVariance
import annotation.constructorOnly
import compiletime.uninitialized
import Decorators._
import Decorators.*
import dotty.tools.dotc.ast.untpd.Tree

object Trees {

Expand Down Expand Up @@ -693,6 +706,8 @@ object Trees {
*/
class InferredTypeTree[+T <: Untyped](implicit @constructorOnly src: SourceFile) extends TypeTree[T]

class ThrowsReturn[+T <: Untyped](val rteTpe : Tree[T], val except : List[Any])(implicit @constructorOnly src: SourceFile) extends TypeTree[T]

/** ref.type */
case class SingletonTypeTree[+T <: Untyped] private[ast] (ref: Tree[T])(implicit @constructorOnly src: SourceFile)
extends DenotingTree[T] with TypTree[T] {
Expand Down
10 changes: 10 additions & 0 deletions compiler/src/dotty/tools/dotc/ast/untpd.scala
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,14 @@ object untpd extends Trees.Instance[Untyped] with UntypedTreeInfo {
case class InterpolatedString(id: TermName, segments: List[Tree])(implicit @constructorOnly src: SourceFile)
extends TermTree

/**
* A Sugar type wrapping the return type and the exceptions that might be thrown by a function
* @param exceptions - Exceptions that might be thrown
* @param rteTpe - Return type of the function
* @param src - source file of this tree
*/
case class ThrowsReturn(exceptions: List[Tree], rteTpe: Tree)(implicit @constructorOnly src: SourceFile) extends Tree

/** A function type or closure */
case class Function(args: List[Tree], body: Tree)(implicit @constructorOnly src: SourceFile) extends Tree {
override def isTerm: Boolean = body.isTerm
Expand Down Expand Up @@ -795,6 +803,8 @@ object untpd extends Trees.Instance[Untyped] with UntypedTreeInfo {
this(x, expr)
case CapturingTypeTree(refs, parent) =>
this(this(x, refs), parent)
case ThrowsReturn(exceptions, rteTpe) =>
x
case _ =>
super.foldMoreCases(x, tree)
}
Expand Down
1 change: 1 addition & 0 deletions compiler/src/dotty/tools/dotc/config/Printers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,5 @@ object Printers {
val typr = noPrinter
val unapp = noPrinter
val variances = noPrinter
val saferExceptions = noPrinter
}
92 changes: 92 additions & 0 deletions compiler/src/dotty/tools/dotc/core/JavaExceptionsInterop.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package dotty.tools.dotc.core

import Contexts.*
import Types.*
import Symbols.*
import Flags.JavaDefined
import Names.*
import dotty.tools.dotc.config.Feature
import dotty.tools.dotc.config.Printers.saferExceptions
import Decorators.i
import dotty.tools.dotc.ast.Trees.ValOrDefDef

/*
TODO : Functionnality purposes
1 - Fetch from the java source (.java or .class) a list of all the thrown exceptions
2 - Create an disjonction type between all of them (use OrType to do that)
3 - Fix the TermName of the synthetic using clause generated in JavaExceptionsInterop::curryCanThrow

TODO : Debuging purposes
1 - Add a new Printer to print information on "safer exceptions"
2 - Use the reporter to report warnings or error that may happen in this step
*/

object JavaExceptionsInterop :

/**
* Return the ClassSymbol that corresponds to
* 'scala.CanThrow' class
* @return
*/
private inline def canThrowType(using Context) = defn.CanThrowClass

/**
* Checks if the "safer exceptions" feature is enables in a file
* To enable "safer exceptions", use this import :
* 'import scala.language.experimental.saferExceptions'
* @return (Boolean) - true is the feature is enabled. False otherwise
*/
def isEnabled(using Context) = Feature.enabled(Feature.saferExceptions)

/**
*
*
* @param sym
* @param tp
* @return
*/
def canThrowMember(sym: Symbol, tp: Type, exceptions : List[Type])(using Context) : Type =
//assert(sym.is(JavaDefined), "can only decorate java defined methods")
assert(isEnabled, "saferExceptions is disabled")
assert(exceptions.nonEmpty, "Cannot append empty exceptions")
saferExceptions.println(i"Synthesizing '$canThrowType' clause for symbol $sym of type: $tp")
println(i"Exceptions to add to the symbol are $exceptions")
println(tp)
tp match
case mtd@MethodType(terms) =>
val synthTp = curryCanThrow(mtd, terms, exceptions)
saferExceptions.println(i"Compiler synthesized type '$synthTp' for symbol $sym")
synthTp
case _ =>
tp


/**
* Add a curried parameters of type CanThrow[Excpetion] to the end of the parameter list
* @rtnType should be any other type
* */
def curryCanThrow(mtd : MethodType, param : List[TermName], exceptions : List[Type])(using Context) : Type =
mtd.resType match
case a@MethodType(terms) =>
CachedMethodType(terms)(_ => mtd.paramInfos, _ => curryCanThrow(a, terms, exceptions), mtd.companion)
case t =>
CachedMethodType(param)(_ => mtd.paramInfos, _ => canThrowMethodType(t, exceptions), mtd.companion)

def canThrowMethodType(resType : Type, exceptions : List[Type])(using Context) =
// First join all the expections with a disjonction
val reduced_exceptions = exceptions.reduce(OrType(_, _, true)) // TODO : Not sure what true does, to check...
saferExceptions.println(i"Joining all the Exception types into one disjonction type '$reduced_exceptions'")

// Apply the disjonction to the CanThrow type parameter
saferExceptions.println(i"Applying the exceptions '$reduced_exceptions' to the polymorphic type '$canThrowType'")
val genericType = canThrowType.typeRef.applyIfParameterized(reduced_exceptions :: Nil)
saferExceptions.println(i"Type of the synthetic '$canThrowType' clause is '$genericType'")

// Build the MethodTypeCompanion
val isErased = canThrowType is Flags.Erased
val companion = MethodType.companion(isContextual = true, isErased = isErased)

// TODO : Maybe use MethodType::fromSymbols instead of creating it from scratch ?
val syntheticClause = CachedMethodType(companion.syntheticParamNames(1))(_ => genericType :: Nil, _ => resType, companion)
saferExceptions.println(i"The synthetic clause is '$syntheticClause'")
syntheticClause
11 changes: 11 additions & 0 deletions compiler/src/dotty/tools/dotc/core/Types.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3502,6 +3502,17 @@ object Types {
/** Like `make`, but also supports higher-kinded types as argument */
def makeHk(tp1: Type, tp2: Type)(using Context): Type =
TypeComparer.liftIfHK(tp1, tp2, OrType(_, _, soft = true), makeHk, _ & _)

/**
* Recursively split an OrType
* @param tp
* @return
*/
def split(tp: Type): List[Type] =
tp match
case OrType(lhs, rhs) => split(lhs) ::: split(rhs)
case _ => tp :: Nil

}

/** An extractor object to pattern match against a nullable union.
Expand Down
27 changes: 23 additions & 4 deletions compiler/src/dotty/tools/dotc/parsing/JavaParsers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ import reporting._
import dotty.tools.dotc.util.SourceFile
import util.Spans._
import scala.collection.mutable.ListBuffer
import scala.runtime.stdLibPatches.language.experimental.saferExceptions
import dotty.tools.dotc.printing.Printer
import dotty.tools.dotc.config.Printers.saferExceptions as sfp

object JavaParsers {

Expand Down Expand Up @@ -541,11 +544,12 @@ object JavaParsers {
}
}

def optThrows(): Unit =
def optThrows(): List[Tree] =
if (in.token == THROWS) {
in.nextToken()
repsep(() => typ(), COMMA)
}
Nil

def methodBody(): Tree = atSpan(in.offset) {
skipAhead()
Expand Down Expand Up @@ -574,11 +578,16 @@ object JavaParsers {
if (in.token == LPAREN && rtptName != nme.EMPTY && !inInterface) {
// constructor declaration
val vparams = formalParams()
optThrows()
val exceptions = optThrows()
val throwsAnnotation =
for exc <- exceptions yield
val annot = genThrowsAnnotation(exc)
sfp.println(i"[JavaParsers] Parser will add ($annot) annotation in ${nme.CONSTRUCTOR}")
annot
List {
atSpan(start) {
DefDef(nme.CONSTRUCTOR, joinParams(tparams, List(vparams)),
TypeTree(), methodBody()).withMods(mods)
TypeTree(), methodBody()).withMods(mods.withAnnotations(throwsAnnotation))
}
}
}
Expand All @@ -591,7 +600,14 @@ object JavaParsers {
// method declaration
val vparams = formalParams()
if (!isVoid) rtpt = optArrayBrackets(rtpt)
optThrows()
val exceptions = optThrows()
mods1 = mods1.withAnnotations{
for exc <- exceptions yield
val annot = genThrowsAnnotation(exc)
sfp.println(i"[JavaParsers] Parser will add ($annot) annotation in $name")
annot
}

val bodyOk = !inInterface || mods.isOneOf(Flags.DefaultMethod | Flags.JavaStatic | Flags.Private)
val body =
if (bodyOk && in.token == LBRACE)
Expand Down Expand Up @@ -628,6 +644,9 @@ object JavaParsers {
}
}

def genThrowsAnnotation(tpe: Tree) : Tree =
Apply(Select(New(Ident(tpnme.throws)), nme.CONSTRUCTOR), tpe :: Nil)

/** Parse a sequence of field declarations, separated by commas.
* This one is tricky because a comma might also appear in an
* initializer. Since we don't parse initializers we don't know
Expand Down
Loading