From 0c261955458df4faa7905c7eff0be25787187b59 Mon Sep 17 00:00:00 2001 From: Hamza REMMAL <56235032+hamzaremmal@users.noreply.github.com> Date: Sun, 20 Nov 2022 13:54:53 +0100 Subject: [PATCH 01/25] Add specific Printer for safer-exceptions --- compiler/src/dotty/tools/dotc/config/Printers.scala | 1 + 1 file changed, 1 insertion(+) diff --git a/compiler/src/dotty/tools/dotc/config/Printers.scala b/compiler/src/dotty/tools/dotc/config/Printers.scala index ecb189de9bb3..57440bfffb5a 100644 --- a/compiler/src/dotty/tools/dotc/config/Printers.scala +++ b/compiler/src/dotty/tools/dotc/config/Printers.scala @@ -49,4 +49,5 @@ object Printers { val typr = noPrinter val unapp = noPrinter val variances = noPrinter + val saferExceptions = new Printer } From 7b30d5cf52ff933bd5a9fc2f9f662eeff82b332a Mon Sep 17 00:00:00 2001 From: Hamza REMMAL <56235032+hamzaremmal@users.noreply.github.com> Date: Sun, 20 Nov 2022 14:06:56 +0100 Subject: [PATCH 02/25] Implement parser for throws declaration - Only methods for now --- .../dotty/tools/dotc/parsing/Parsers.scala | 56 ++++++++++++------- tests/safer-exceptions/neg/t01.scala | 5 ++ tests/safer-exceptions/pos/t01.scala | 6 ++ 3 files changed, 46 insertions(+), 21 deletions(-) create mode 100644 tests/safer-exceptions/neg/t01.scala create mode 100644 tests/safer-exceptions/pos/t01.scala diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index a198cccc85cc..892d4e0f5aa0 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -3,35 +3,36 @@ package dotc package parsing import scala.language.unsafeNulls - import scala.annotation.internal.sharable import scala.collection.mutable.ListBuffer import scala.collection.immutable.BitSet -import util.{ SourceFile, SourcePosition, NoSourcePosition } -import Tokens._ -import Scanners._ +import util.{NoSourcePosition, SourceFile, SourcePosition} +import Tokens.* +import Scanners.* import xml.MarkupParsers.MarkupParser -import core._ -import Flags._ -import Contexts._ -import Names._ -import NameKinds.{WildcardParamName, QualifiedName} -import NameOps._ +import core.* +import Flags.* +import Contexts.* +import Names.* +import NameKinds.{QualifiedName, WildcardParamName} +import NameOps.* import ast.{Positioned, Trees} -import ast.Trees._ -import StdNames._ -import util.Spans._ -import Constants._ +import ast.Trees.* +import StdNames.* +import util.Spans.* +import Constants.* import Symbols.NoSymbol -import ScriptParsers._ -import Decorators._ +import ScriptParsers.* +import Decorators.* import util.Chars + import scala.annotation.tailrec -import rewrites.Rewrites.{patch, overlapsPatch} -import reporting._ +import rewrites.Rewrites.{overlapsPatch, patch} +import reporting.* import config.Feature -import config.Feature.{sourceVersion, migrateTo3, globalOnlyImports} -import config.SourceVersion._ +import config.Feature.{globalOnlyImports, migrateTo3, saferExceptions, sourceVersion} +import config.Printers +import config.SourceVersion.* import config.SourceVersion object Parsers { @@ -3555,9 +3556,10 @@ object Parsers { else { val mods1 = addFlag(mods, Method) val ident = termIdent() - var name = ident.name.asTermName + val name = ident.name.asTermName val tparams = typeParamClauseOpt(ParamOwner.Def) val vparamss = paramClauses(numLeadParams = numLeadParams) + val exceptions = throwsClauseOpt var tpt = fromWithinReturnType { typedOpt() } if (migrateTo3) newLineOptWhenFollowedBy(LBRACE) val rhs = @@ -3581,6 +3583,18 @@ object Parsers { } } + def throwsClauseOpt : List[Tree] = + if in.featureEnabled(Feature.saferExceptions) then + if(isIdent(nme.throws)) + in.nextToken() + val except = commaSeparated(() => toplevelTyp()) + Printers.saferExceptions.println(i"exceptions thrown are : $except") + except + else + Nil + else + Nil + /** ConstrExpr ::= SelfInvocation * | `{' SelfInvocation {semi BlockStat} `}' */ diff --git a/tests/safer-exceptions/neg/t01.scala b/tests/safer-exceptions/neg/t01.scala new file mode 100644 index 000000000000..cf45d5c183d5 --- /dev/null +++ b/tests/safer-exceptions/neg/t01.scala @@ -0,0 +1,5 @@ +import java.io.IOException + +class GenericExc[T] extends Exception + +def test throws IOException, GenericExc[Int] : Unit = () \ No newline at end of file diff --git a/tests/safer-exceptions/pos/t01.scala b/tests/safer-exceptions/pos/t01.scala new file mode 100644 index 000000000000..5ffe5881456b --- /dev/null +++ b/tests/safer-exceptions/pos/t01.scala @@ -0,0 +1,6 @@ +import language.experimental.saferExceptions +import java.io.IOException + +class GenericExc[T] extends Exception + +def test throws IOException, GenericExc[Int] : Unit = () \ No newline at end of file From bc729593ecd3389bd80d48502597b4d19188fb7a Mon Sep 17 00:00:00 2001 From: Hamza REMMAL <56235032+hamzaremmal@users.noreply.github.com> Date: Sat, 26 Nov 2022 14:10:54 +0100 Subject: [PATCH 03/25] Desugar a throws declaration as a contextual function --- .../src/dotty/tools/dotc/ast/Desugar.scala | 5 ++++ compiler/src/dotty/tools/dotc/ast/Trees.scala | 27 ++++++++++++++----- compiler/src/dotty/tools/dotc/ast/untpd.scala | 10 +++++++ .../dotty/tools/dotc/parsing/Parsers.scala | 15 +++++++++-- 4 files changed, 49 insertions(+), 8 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/ast/Desugar.scala b/compiler/src/dotty/tools/dotc/ast/Desugar.scala index 1e1db19bcf25..22c2592a8989 100644 --- a/compiler/src/dotty/tools/dotc/ast/Desugar.scala +++ b/compiler/src/dotty/tools/dotc/ast/Desugar.scala @@ -1765,6 +1765,11 @@ object desugar { } val desugared = tree match { + case ThrowsReturn(exceptions, rteTpe) => + val r = exceptions.reduce((left, right) => InfixOp(left, Ident(nme.OR.toTypeName), right)) + val args = AppliedTypeTree(Ident(defn.CanThrowClass.name), r) + Printers.saferExceptions.println(i"$args") + FunctionWithMods(args :: Nil, rteTpe, Modifiers(Flags.Given)) case PolyFunction(targs, body) => makePolyFunction(targs, body, pt) orElse tree case SymbolLit(str) => diff --git a/compiler/src/dotty/tools/dotc/ast/Trees.scala b/compiler/src/dotty/tools/dotc/ast/Trees.scala index 253477c5382c..7dd6948ff099 100644 --- a/compiler/src/dotty/tools/dotc/ast/Trees.scala +++ b/compiler/src/dotty/tools/dotc/ast/Trees.scala @@ -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 { @@ -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] { diff --git a/compiler/src/dotty/tools/dotc/ast/untpd.scala b/compiler/src/dotty/tools/dotc/ast/untpd.scala index f72cafd4205d..860611ef358b 100644 --- a/compiler/src/dotty/tools/dotc/ast/untpd.scala +++ b/compiler/src/dotty/tools/dotc/ast/untpd.scala @@ -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 @@ -787,6 +795,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) } diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index 892d4e0f5aa0..6dcae183a07e 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -3576,13 +3576,24 @@ object Parsers { if (!isExprIntro) syntaxError(MissingReturnType(), in.lastOffset) accept(EQUALS) expr() - - val ddef = DefDef(name, joinParams(tparams, vparamss), tpt, rhs) + val tpe = + if exceptions.nonEmpty then + ThrowsReturn(exceptions, tpt) + else + tpt + val ddef = DefDef(name, joinParams(tparams, vparamss), tpe, rhs) if (isBackquoted(ident)) ddef.pushAttachment(Backquoted, ()) finalizeDef(ddef, mods1, start) } } + /** + * Parse a 'throws' declaration. + * If the saferException feature is disabled, this function + * will return Nil. + * ThrowsClause := (throws toplevelTyp (, toplevelTyp)*)? + * @return A List of all the exceptions allowed to be thrown + */ def throwsClauseOpt : List[Tree] = if in.featureEnabled(Feature.saferExceptions) then if(isIdent(nme.throws)) From 429108506abfeeaef89ce4e831d134d622343e85 Mon Sep 17 00:00:00 2001 From: Hamza REMMAL <56235032+hamzaremmal@users.noreply.github.com> Date: Sat, 26 Nov 2022 14:14:59 +0100 Subject: [PATCH 04/25] First draft of the safer-exceptions tests --- tests/safer-exceptions/neg/t01.scala | 10 +++++++--- tests/safer-exceptions/pos/t01.scala | 18 ++++++++++++++++-- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/tests/safer-exceptions/neg/t01.scala b/tests/safer-exceptions/neg/t01.scala index cf45d5c183d5..d48aa787f16e 100644 --- a/tests/safer-exceptions/neg/t01.scala +++ b/tests/safer-exceptions/neg/t01.scala @@ -1,5 +1,9 @@ import java.io.IOException -class GenericExc[T] extends Exception - -def test throws IOException, GenericExc[Int] : Unit = () \ No newline at end of file +def test throws IOException, GenericExc[Int] : Unit = () +/* +-- [E040] Syntax Error: tests\safer-exceptions\neg\t01.scala:5:9 --------------- + 5 |def test throws IOException, GenericExc[Int] : Unit = () + | ^^^^^^ + | '=' expected, but identifier found +*/ \ No newline at end of file diff --git a/tests/safer-exceptions/pos/t01.scala b/tests/safer-exceptions/pos/t01.scala index 5ffe5881456b..3480bfdd136d 100644 --- a/tests/safer-exceptions/pos/t01.scala +++ b/tests/safer-exceptions/pos/t01.scala @@ -1,6 +1,20 @@ -import language.experimental.saferExceptions +package test.saferExceptions.pos + +import language.experimental.{saferExceptions} import java.io.IOException +import java.io.FileNotFoundException class GenericExc[T] extends Exception -def test throws IOException, GenericExc[Int] : Unit = () \ No newline at end of file +def test throws IOException, FileNotFoundException :Unit = throw new IOException("") + +// Main method cannot have implicit parameters nor return typpe +// Why ? and how can we avoid this ? +//@main def a(args : Array[String]) throws IOException : Unit = +// test + +trait AFoo : + def foo(x : Int) throws Exception : Int + +class Foo extends AFoo : + def foo(x : Int) throws IOException, FileNotFoundException : Int = x From dcf75b59b9071f987520ee391f1ae8caad8da13f Mon Sep 17 00:00:00 2001 From: Hamza REMMAL <56235032+hamzaremmal@users.noreply.github.com> Date: Sat, 26 Nov 2022 20:38:30 +0100 Subject: [PATCH 05/25] Implement a warning when a Java method is called in a context where safer-exceptions is enabled. --- .../dotty/tools/dotc/config/Printers.scala | 2 +- .../dotty/tools/dotc/typer/Applications.scala | 22 +++++++++++++++++-- .../src/dotty/tools/dotc/typer/Typer.scala | 10 +++++++++ tests/safer-exceptions/pos/JavaTestFile.java | 9 ++++++++ tests/safer-exceptions/pos/t01.scala | 15 ++++++++++--- tests/safer-exceptions/pos/t02.scala | 15 +++++++++++++ 6 files changed, 67 insertions(+), 6 deletions(-) create mode 100644 tests/safer-exceptions/pos/JavaTestFile.java create mode 100644 tests/safer-exceptions/pos/t02.scala diff --git a/compiler/src/dotty/tools/dotc/config/Printers.scala b/compiler/src/dotty/tools/dotc/config/Printers.scala index 57440bfffb5a..de1694cd16e5 100644 --- a/compiler/src/dotty/tools/dotc/config/Printers.scala +++ b/compiler/src/dotty/tools/dotc/config/Printers.scala @@ -49,5 +49,5 @@ object Printers { val typr = noPrinter val unapp = noPrinter val variances = noPrinter - val saferExceptions = new Printer + val saferExceptions = noPrinter } diff --git a/compiler/src/dotty/tools/dotc/typer/Applications.scala b/compiler/src/dotty/tools/dotc/typer/Applications.scala index ec72c48b2422..c87d69e321b4 100644 --- a/compiler/src/dotty/tools/dotc/typer/Applications.scala +++ b/compiler/src/dotty/tools/dotc/typer/Applications.scala @@ -902,12 +902,20 @@ trait Applications extends Compatibility { if (ctx.owner.isClassConstructor && untpd.isSelfConstrCall(app)) ctx.thisCallArgContext else ctx + // TODO HR : Change this to check for a CanThrow Capability for a Java function call + /* + (1) - How to fetch the Exceptions from a Type definition + I think it may have smtg to do with the Annotations + (2) - How to require a given object. Try to find how given are fetched from the context when + The compiler tries to fetch them for contextual functions + (3) - Do it :-) + */ + /** Typecheck application. Result could be an `Apply` node, * or, if application is an operator assignment, also an `Assign` or * Block node. */ def typedApply(tree: untpd.Apply, pt: Type)(using Context): Tree = { - def realApply(using Context): Tree = { val resultProto = tree.fun match case Select(New(tpt), _) if pt.isInstanceOf[ValueType] => @@ -925,7 +933,17 @@ trait Applications extends Compatibility { new FunProto(tree.args, resultProto)(this, tree.applyKind)(using argCtx(tree)) record("typedApply") val fun1 = typedExpr(tree.fun, originalProto) - + // TODO HR : Add this point, we can fetch the denotation from fun1 + // TODO HR : If it has as annotations the ThrowsAnnotation, then request + // TODO HR : a given instance.Otherwise, continue processing + // TODO HR : Weird thing, How overloading is handled ?? + if fun1.symbol.is(JavaDefined) && Feature.enabled(Feature.saferExceptions) then + // TODO HR : What's the difference between i and em interpolator. (I know that i uses the the show method, how about em ?) + report.warning( + em""" A Java function was called (${fun1.symbol.name}) in a context where safer exceptions is enabled. + | This function might throw an exception. + | Handling of Java method is yet to be implemented. + |""".stripMargin, tree.srcPos) // If adaptation created a tupled dual of `originalProto`, pick the right version // (tupled or not) of originalProto to proceed. val proto = diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 032bed38482c..0dc39e9f2e01 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -1277,6 +1277,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer if (ctx.mode is Mode.Type) typedFunctionType(tree, pt) else typedFunctionValue(tree, pt) + // TODO HR : Change this function to infer the return type before desugaring a throws clause def typedFunctionType(tree: untpd.Function, pt: Type)(using Context): Tree = { val untpd.Function(args, body) = tree var funFlags = tree match { @@ -1342,6 +1343,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer } } + // TODO HR : Change this function to infer the return type before desugaring a throws clause def typedFunctionValue(tree: untpd.Function, pt: Type)(using Context): Tree = { val untpd.Function(params: List[untpd.ValDef] @unchecked, _) = tree: @unchecked @@ -1857,6 +1859,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer val capabilityProof = caughtExceptions.reduce(OrType(_, _, true)) untpd.Block(makeCanThrow(capabilityProof), expr) + // TODO HR : Change this to infer all the CanThrow capabilities instead of the desugar def typedTry(tree: untpd.Try, pt: Type)(using Context): Try = { val expr2 :: cases2x = harmonic(harmonize, pt) { val cases1 = typedCases(tree.cases, EmptyTree, defn.ThrowableType, pt.dropIfProto) @@ -1868,6 +1871,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer assignType(cpy.Try(tree)(expr2, cases2, finalizer1), expr2, cases2) } + // TODO HR : Change this to infer all the CanThrow capabilities instead of the desugar def typedTry(tree: untpd.ParsedTry, pt: Type)(using Context): Try = val cases: List[untpd.CaseDef] = tree.handler match case Match(EmptyTree, cases) => cases @@ -1877,6 +1881,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer desugar.makeTryCase(handler1) :: Nil typedTry(untpd.Try(tree.expr, cases, tree.finalizer).withSpan(tree.span), pt) + // TODO HR : Maybe change it if we want to infer the throws annotation def typedThrow(tree: untpd.Throw)(using Context): Tree = val expr1 = typed(tree.expr, defn.ThrowableType) val cap = checkCanThrow(expr1.tpe.widen, tree.span) @@ -1982,6 +1987,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer assignType(cpy.RefinedTypeTree(tree)(tpt1, refinements1), tpt1, refinements1, refineCls) } + // TODO HR : Remove anything related to the infix type $throws def typedAppliedTypeTree(tree: untpd.AppliedTypeTree)(using Context): Tree = { tree.args match case arg :: _ if arg.isTerm => @@ -2306,6 +2312,8 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer vdef1.setDefTree } + // TODO HR : Should we modifiy this function, + // TODO HR : Yes, if we want to infer the throws clauses too def typedDefDef(ddef: untpd.DefDef, sym: Symbol)(using Context): Tree = { if (!sym.info.exists) { // it's a discarded synthetic case class method, drop it assert(sym.is(Synthetic) && desugar.isRetractableCaseClassMethodName(sym.name)) @@ -2774,6 +2782,8 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer val untpd.InfixOp(l, op, r) = tree val result = if (ctx.mode.is(Mode.Type)) + // TODO HR : This code should be removed as well as the infix $throws type + // TODO HR : Throws clauses are part of the scala syntax now typedAppliedTypeTree( if op.name == tpnme.throws && Feature.enabled(Feature.saferExceptions) then desugar.throws(l, op, r) diff --git a/tests/safer-exceptions/pos/JavaTestFile.java b/tests/safer-exceptions/pos/JavaTestFile.java new file mode 100644 index 000000000000..63c826374351 --- /dev/null +++ b/tests/safer-exceptions/pos/JavaTestFile.java @@ -0,0 +1,9 @@ +package test.saferExceptions.pos; + +import java.io.IOException; + +public class JavaTestFile { + + public static void test() throws IOException {} + +} \ No newline at end of file diff --git a/tests/safer-exceptions/pos/t01.scala b/tests/safer-exceptions/pos/t01.scala index 3480bfdd136d..9afab89e1dbc 100644 --- a/tests/safer-exceptions/pos/t01.scala +++ b/tests/safer-exceptions/pos/t01.scala @@ -1,12 +1,21 @@ package test.saferExceptions.pos -import language.experimental.{saferExceptions} +import language.experimental.saferExceptions import java.io.IOException +import java.io.FileInputStream import java.io.FileNotFoundException -class GenericExc[T] extends Exception +class ScalaException extends Exception -def test throws IOException, FileNotFoundException :Unit = throw new IOException("") +def test() throws ScalaException :Unit = throw new ScalaException +/* +-- Warning: tests\safer-exceptions\pos\t01.scala:8:29 -------------------------- +8 |class ScalaException extends Exception + | ^^^^^^^^^ + | A Java function was called () in a context where safer exceptions is enabled. + | This function might throw an exception. + | Handling of Java method is yet to be implemented. +*/ // Main method cannot have implicit parameters nor return typpe // Why ? and how can we avoid this ? diff --git a/tests/safer-exceptions/pos/t02.scala b/tests/safer-exceptions/pos/t02.scala new file mode 100644 index 000000000000..b144e395ba23 --- /dev/null +++ b/tests/safer-exceptions/pos/t02.scala @@ -0,0 +1,15 @@ +package test.saferExceptions.pos + +import language.experimental.saferExceptions +import test.saferExceptions.pos.JavaTestFile + +def checkJavaFunction : Unit = JavaTestFile.test() +/* +-- Warning: tests\safer-exceptions\pos\t02.scala:6:48 -------------------------- +6 |def checkJavaFunction : Unit = JavaTestFile.test() + | ^^^^^^^^^^^^^^^^^^^ + | A Java function was called (test) in a context where safer exceptions is enabled. + | This function might throw an exception. + | Handling of Java method is yet to be implemented. + +*/ From bb611c30204c3c1664aeeccdb7c5fe754dfd9327 Mon Sep 17 00:00:00 2001 From: Hamza REMMAL <56235032+hamzaremmal@users.noreply.github.com> Date: Sat, 26 Nov 2022 20:39:09 +0100 Subject: [PATCH 06/25] Add legacy code related to safer-exceptions. --- .../dotc/core/JavaExceptionsInterop.scala | 92 +++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 compiler/src/dotty/tools/dotc/core/JavaExceptionsInterop.scala diff --git a/compiler/src/dotty/tools/dotc/core/JavaExceptionsInterop.scala b/compiler/src/dotty/tools/dotc/core/JavaExceptionsInterop.scala new file mode 100644 index 000000000000..3e5e3bb3dc83 --- /dev/null +++ b/compiler/src/dotty/tools/dotc/core/JavaExceptionsInterop.scala @@ -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 From 8c509f3ad10e5652a9aa4ecf07631e5a8798f31c Mon Sep 17 00:00:00 2001 From: Hamza REMMAL <56235032+hamzaremmal@users.noreply.github.com> Date: Sat, 26 Nov 2022 21:34:35 +0100 Subject: [PATCH 07/25] Use the correct Or name in Desugar.scala --- compiler/src/dotty/tools/dotc/ast/Desugar.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/ast/Desugar.scala b/compiler/src/dotty/tools/dotc/ast/Desugar.scala index 22c2592a8989..fda30173c619 100644 --- a/compiler/src/dotty/tools/dotc/ast/Desugar.scala +++ b/compiler/src/dotty/tools/dotc/ast/Desugar.scala @@ -1766,7 +1766,7 @@ object desugar { val desugared = tree match { case ThrowsReturn(exceptions, rteTpe) => - val r = exceptions.reduce((left, right) => InfixOp(left, Ident(nme.OR.toTypeName), right)) + val r = exceptions.reduce((left, right) => InfixOp(left, Ident(tpnme.OR), right)) val args = AppliedTypeTree(Ident(defn.CanThrowClass.name), r) Printers.saferExceptions.println(i"$args") FunctionWithMods(args :: Nil, rteTpe, Modifiers(Flags.Given)) From 079c7b121bc83bb6fd22d25af801ad2e7838d47f Mon Sep 17 00:00:00 2001 From: Hamza REMMAL <56235032+hamzaremmal@users.noreply.github.com> Date: Tue, 13 Dec 2022 13:36:29 +0100 Subject: [PATCH 08/25] Implement the checks for CanThrow capabilities in Applications.scala --- .../dotty/tools/dotc/typer/Applications.scala | 75 ++++++++++--------- 1 file changed, 39 insertions(+), 36 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/typer/Applications.scala b/compiler/src/dotty/tools/dotc/typer/Applications.scala index c87d69e321b4..1caa485c0e88 100644 --- a/compiler/src/dotty/tools/dotc/typer/Applications.scala +++ b/compiler/src/dotty/tools/dotc/typer/Applications.scala @@ -2,39 +2,41 @@ package dotty.tools package dotc package typer -import core._ -import ast.{Trees, tpd, untpd, desugar} +import core.* +import ast.{Trees, desugar, tpd, untpd} import util.Stats.record -import util.{SrcPos, NoSourcePosition} -import Contexts._ -import Flags._ -import Symbols._ +import util.{NoSourcePosition, SrcPos} +import Contexts.* +import dotty.tools.dotc.config.Printers.saferExceptions +import Flags.* +import Symbols.* import Denotations.Denotation -import Types._ -import Decorators._ -import ErrorReporting._ -import Trees._ -import Names._ -import StdNames._ -import ContextOps._ +import Types.* +import Decorators.* +import ErrorReporting.* +import Trees.* +import Names.* +import StdNames.* +import ContextOps.* import NameKinds.DefaultGetterName -import ProtoTypes._ -import Inferencing._ -import reporting._ -import transform.TypeUtils._ -import transform.SymUtils._ -import Nullables._ +import ProtoTypes.* +import Inferencing.* +import reporting.* +import transform.TypeUtils.* +import transform.SymUtils.* +import Nullables.* +import Checking.* import config.Feature import collection.mutable import config.Printers.{overload, typr, unapp} -import TypeApplications._ -import Annotations.Annotation - +import TypeApplications.* +import Annotations.{Annotation, ThrownException, ThrowsAnnotation} import Constants.{Constant, IntTag} import Denotations.SingleDenotation -import annotation.threadUnsafe +import dotty.tools.dotc.typer.Implicits.{SearchFailure, SearchSuccess} +import annotation.threadUnsafe import scala.util.control.NonFatal object Applications { @@ -929,21 +931,22 @@ trait Applications extends Compatibility { case _ => IgnoredProto(pt) // Do ignore other expected result types, since there might be an implicit conversion // on the result. We could drop this if we disallow unrestricted implicit conversions. - val originalProto = - new FunProto(tree.args, resultProto)(this, tree.applyKind)(using argCtx(tree)) + val originalProto = FunProto(tree.args, resultProto)(this, tree.applyKind)(using argCtx(tree)) record("typedApply") val fun1 = typedExpr(tree.fun, originalProto) - // TODO HR : Add this point, we can fetch the denotation from fun1 - // TODO HR : If it has as annotations the ThrowsAnnotation, then request - // TODO HR : a given instance.Otherwise, continue processing - // TODO HR : Weird thing, How overloading is handled ?? - if fun1.symbol.is(JavaDefined) && Feature.enabled(Feature.saferExceptions) then - // TODO HR : What's the difference between i and em interpolator. (I know that i uses the the show method, how about em ?) - report.warning( - em""" A Java function was called (${fun1.symbol.name}) in a context where safer exceptions is enabled. - | This function might throw an exception. - | Handling of Java method is yet to be implemented. - |""".stripMargin, tree.srcPos) + if Feature.enabled(Feature.saferExceptions) then { + val sym: Symbol = fun1.symbol + val annot = sym.annotations + val throwsAnnot = annot.filter(ThrownException.unapply(_).isDefined) + val exceptions = throwsAnnot.map(ThrownException.unapply(_).get) + val capabities = for e <- exceptions yield checkCanThrow(e, tree.span) + if exceptions.nonEmpty then + report.warning( + i""" A function was called (${sym.name}) in a context where safer exceptions is enabled. + | This function throws an exception. + | ${exceptions zip capabities} + |""".stripMargin, tree.srcPos) + } // If adaptation created a tupled dual of `originalProto`, pick the right version // (tupled or not) of originalProto to proceed. val proto = From 6fda15f68220f700eb06187d67857eb410e33124 Mon Sep 17 00:00:00 2001 From: Hamza REMMAL <56235032+hamzaremmal@users.noreply.github.com> Date: Tue, 13 Dec 2022 15:16:13 +0100 Subject: [PATCH 09/25] Split union types when checking for capabilities. --- compiler/src/dotty/tools/dotc/typer/Applications.scala | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/typer/Applications.scala b/compiler/src/dotty/tools/dotc/typer/Applications.scala index 1caa485c0e88..6990b6f5a2cf 100644 --- a/compiler/src/dotty/tools/dotc/typer/Applications.scala +++ b/compiler/src/dotty/tools/dotc/typer/Applications.scala @@ -918,6 +918,12 @@ trait Applications extends Compatibility { * Block node. */ def typedApply(tree: untpd.Apply, pt: Type)(using Context): Tree = { + + def splitOr(tpe: Type): List[Type] = + tpe match + case OrType(lhs, rhs) => splitOr(lhs) ::: splitOr(rhs) + case _ => tpe :: Nil + def realApply(using Context): Tree = { val resultProto = tree.fun match case Select(New(tpt), _) if pt.isInstanceOf[ValueType] => @@ -938,7 +944,7 @@ trait Applications extends Compatibility { val sym: Symbol = fun1.symbol val annot = sym.annotations val throwsAnnot = annot.filter(ThrownException.unapply(_).isDefined) - val exceptions = throwsAnnot.map(ThrownException.unapply(_).get) + val exceptions = throwsAnnot.map(ThrownException.unapply(_).get).flatMap(splitOr) val capabities = for e <- exceptions yield checkCanThrow(e, tree.span) if exceptions.nonEmpty then report.warning( From 9956d5b71751b1337eba29f485f8720c38f79711 Mon Sep 17 00:00:00 2001 From: Hamza REMMAL <56235032+hamzaremmal@users.noreply.github.com> Date: Tue, 13 Dec 2022 15:18:05 +0100 Subject: [PATCH 10/25] Correct StringInterpolator in warning message related to saferexceptions --- compiler/src/dotty/tools/dotc/typer/Applications.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/src/dotty/tools/dotc/typer/Applications.scala b/compiler/src/dotty/tools/dotc/typer/Applications.scala index 6990b6f5a2cf..07ecbc1bedd9 100644 --- a/compiler/src/dotty/tools/dotc/typer/Applications.scala +++ b/compiler/src/dotty/tools/dotc/typer/Applications.scala @@ -948,7 +948,7 @@ trait Applications extends Compatibility { val capabities = for e <- exceptions yield checkCanThrow(e, tree.span) if exceptions.nonEmpty then report.warning( - i""" A function was called (${sym.name}) in a context where safer exceptions is enabled. + em""" A function was called (${sym.name}) in a context where safer exceptions is enabled. | This function throws an exception. | ${exceptions zip capabities} |""".stripMargin, tree.srcPos) From 19cc4b4c21eb90aa6281c1350b1169fb3c3ef489 Mon Sep 17 00:00:00 2001 From: Hamza REMMAL <56235032+hamzaremmal@users.noreply.github.com> Date: Tue, 13 Dec 2022 15:21:17 +0100 Subject: [PATCH 11/25] Add call to saferExceptions printer for debugging purposes. --- compiler/src/dotty/tools/dotc/typer/Applications.scala | 2 ++ 1 file changed, 2 insertions(+) diff --git a/compiler/src/dotty/tools/dotc/typer/Applications.scala b/compiler/src/dotty/tools/dotc/typer/Applications.scala index 07ecbc1bedd9..b37df60f8ba4 100644 --- a/compiler/src/dotty/tools/dotc/typer/Applications.scala +++ b/compiler/src/dotty/tools/dotc/typer/Applications.scala @@ -945,7 +945,9 @@ trait Applications extends Compatibility { val annot = sym.annotations val throwsAnnot = annot.filter(ThrownException.unapply(_).isDefined) val exceptions = throwsAnnot.map(ThrownException.unapply(_).get).flatMap(splitOr) + saferExceptions.println(i"symbol $sym throws $exceptions") val capabities = for e <- exceptions yield checkCanThrow(e, tree.span) + saferExceptions.println(i"fetch capabilities $capabities to satisfy conditions of $sym") if exceptions.nonEmpty then report.warning( em""" A function was called (${sym.name}) in a context where safer exceptions is enabled. From 9bc67cd43af51dad43f60edc090342f948b5866f Mon Sep 17 00:00:00 2001 From: Hamza REMMAL <56235032+hamzaremmal@users.noreply.github.com> Date: Tue, 13 Dec 2022 15:27:03 +0100 Subject: [PATCH 12/25] Clean code in Typer.scala --- compiler/src/dotty/tools/dotc/typer/Typer.scala | 8 -------- 1 file changed, 8 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 0dc39e9f2e01..3964cd3814cc 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -1277,7 +1277,6 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer if (ctx.mode is Mode.Type) typedFunctionType(tree, pt) else typedFunctionValue(tree, pt) - // TODO HR : Change this function to infer the return type before desugaring a throws clause def typedFunctionType(tree: untpd.Function, pt: Type)(using Context): Tree = { val untpd.Function(args, body) = tree var funFlags = tree match { @@ -1343,7 +1342,6 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer } } - // TODO HR : Change this function to infer the return type before desugaring a throws clause def typedFunctionValue(tree: untpd.Function, pt: Type)(using Context): Tree = { val untpd.Function(params: List[untpd.ValDef] @unchecked, _) = tree: @unchecked @@ -1859,7 +1857,6 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer val capabilityProof = caughtExceptions.reduce(OrType(_, _, true)) untpd.Block(makeCanThrow(capabilityProof), expr) - // TODO HR : Change this to infer all the CanThrow capabilities instead of the desugar def typedTry(tree: untpd.Try, pt: Type)(using Context): Try = { val expr2 :: cases2x = harmonic(harmonize, pt) { val cases1 = typedCases(tree.cases, EmptyTree, defn.ThrowableType, pt.dropIfProto) @@ -1871,7 +1868,6 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer assignType(cpy.Try(tree)(expr2, cases2, finalizer1), expr2, cases2) } - // TODO HR : Change this to infer all the CanThrow capabilities instead of the desugar def typedTry(tree: untpd.ParsedTry, pt: Type)(using Context): Try = val cases: List[untpd.CaseDef] = tree.handler match case Match(EmptyTree, cases) => cases @@ -1881,7 +1877,6 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer desugar.makeTryCase(handler1) :: Nil typedTry(untpd.Try(tree.expr, cases, tree.finalizer).withSpan(tree.span), pt) - // TODO HR : Maybe change it if we want to infer the throws annotation def typedThrow(tree: untpd.Throw)(using Context): Tree = val expr1 = typed(tree.expr, defn.ThrowableType) val cap = checkCanThrow(expr1.tpe.widen, tree.span) @@ -1987,7 +1982,6 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer assignType(cpy.RefinedTypeTree(tree)(tpt1, refinements1), tpt1, refinements1, refineCls) } - // TODO HR : Remove anything related to the infix type $throws def typedAppliedTypeTree(tree: untpd.AppliedTypeTree)(using Context): Tree = { tree.args match case arg :: _ if arg.isTerm => @@ -2312,8 +2306,6 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer vdef1.setDefTree } - // TODO HR : Should we modifiy this function, - // TODO HR : Yes, if we want to infer the throws clauses too def typedDefDef(ddef: untpd.DefDef, sym: Symbol)(using Context): Tree = { if (!sym.info.exists) { // it's a discarded synthetic case class method, drop it assert(sym.is(Synthetic) && desugar.isRetractableCaseClassMethodName(sym.name)) From a17efaefc3e058860751c445c5c6029d189542cb Mon Sep 17 00:00:00 2001 From: Hamza REMMAL <56235032+hamzaremmal@users.noreply.github.com> Date: Tue, 13 Dec 2022 15:39:41 +0100 Subject: [PATCH 13/25] Add missing throws annotation in JavaParsers.scala --- .../tools/dotc/parsing/JavaParsers.scala | 27 ++++++++++++++++--- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/parsing/JavaParsers.scala b/compiler/src/dotty/tools/dotc/parsing/JavaParsers.scala index 183845fcf3ec..32a63cd90074 100644 --- a/compiler/src/dotty/tools/dotc/parsing/JavaParsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/JavaParsers.scala @@ -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 { @@ -543,11 +546,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() @@ -576,11 +580,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)) } } } @@ -593,7 +602,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) @@ -630,6 +646,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 From fd8eafb6e56bf52edeb008b60e2f9c5245e4d645 Mon Sep 17 00:00:00 2001 From: Hamza REMMAL <56235032+hamzaremmal@users.noreply.github.com> Date: Tue, 13 Dec 2022 15:57:13 +0100 Subject: [PATCH 14/25] Fix some of the printer calls for saferExceptions --- compiler/src/dotty/tools/dotc/ast/Desugar.scala | 5 +++-- compiler/src/dotty/tools/dotc/parsing/Parsers.scala | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/ast/Desugar.scala b/compiler/src/dotty/tools/dotc/ast/Desugar.scala index fda30173c619..69aa74ddd400 100644 --- a/compiler/src/dotty/tools/dotc/ast/Desugar.scala +++ b/compiler/src/dotty/tools/dotc/ast/Desugar.scala @@ -1768,8 +1768,9 @@ object desugar { case ThrowsReturn(exceptions, rteTpe) => val r = exceptions.reduce((left, right) => InfixOp(left, Ident(tpnme.OR), right)) val args = AppliedTypeTree(Ident(defn.CanThrowClass.name), r) - Printers.saferExceptions.println(i"$args") - FunctionWithMods(args :: Nil, rteTpe, Modifiers(Flags.Given)) + val fn = FunctionWithMods(args :: Nil, rteTpe, Modifiers(Flags.Given)) + Printers.saferExceptions.println(i"throws clased desugared to $fn") + fn case PolyFunction(targs, body) => makePolyFunction(targs, body, pt) orElse tree case SymbolLit(str) => diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index 6dcae183a07e..b0e073eebc95 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -3599,7 +3599,7 @@ object Parsers { if(isIdent(nme.throws)) in.nextToken() val except = commaSeparated(() => toplevelTyp()) - Printers.saferExceptions.println(i"exceptions thrown are : $except") + Printers.saferExceptions.println(i"Parser will add a throws clause for the given exceptions : $except") except else Nil From 3b7bc882d04dad920eb93317861347dd0ab7afac Mon Sep 17 00:00:00 2001 From: Hamza REMMAL <56235032+hamzaremmal@users.noreply.github.com> Date: Mon, 2 Jan 2023 17:37:46 +0100 Subject: [PATCH 15/25] First draft of splitting CanThrow implicit search --- .../dotty/tools/dotc/config/Printers.scala | 2 +- .../src/dotty/tools/dotc/typer/Checking.scala | 40 ++++++------ .../dotty/tools/dotc/typer/Implicits.scala | 62 ++++++++++++------- tests/safer-exceptions/pos/JavaTestFile.java | 3 +- tests/safer-exceptions/pos/t01.scala | 7 ++- tests/safer-exceptions/pos/t02.scala | 12 +++- tests/safer-exceptions/pos/t03.scala | 13 ++++ 7 files changed, 91 insertions(+), 48 deletions(-) create mode 100644 tests/safer-exceptions/pos/t03.scala diff --git a/compiler/src/dotty/tools/dotc/config/Printers.scala b/compiler/src/dotty/tools/dotc/config/Printers.scala index de1694cd16e5..57440bfffb5a 100644 --- a/compiler/src/dotty/tools/dotc/config/Printers.scala +++ b/compiler/src/dotty/tools/dotc/config/Printers.scala @@ -49,5 +49,5 @@ object Printers { val typr = noPrinter val unapp = noPrinter val variances = noPrinter - val saferExceptions = noPrinter + val saferExceptions = new Printer } diff --git a/compiler/src/dotty/tools/dotc/typer/Checking.scala b/compiler/src/dotty/tools/dotc/typer/Checking.scala index 99399832085f..a4dcfc4c4a47 100644 --- a/compiler/src/dotty/tools/dotc/typer/Checking.scala +++ b/compiler/src/dotty/tools/dotc/typer/Checking.scala @@ -2,46 +2,45 @@ package dotty.tools package dotc package typer -import core._ -import ast._ -import Contexts._ -import Types._ -import Flags._ -import Names._ -import StdNames._ -import Symbols._ -import Trees._ -import ProtoTypes._ -import Scopes._ -import CheckRealizable._ +import core.* +import ast.* +import Contexts.* +import Types.* +import Flags.* +import Names.* +import StdNames.* +import Symbols.* +import Trees.* +import ProtoTypes.* +import Scopes.* +import CheckRealizable.* import ErrorReporting.errorTree import util.Spans.Span import Phases.refchecksPhase import Constants.Constant - import util.SrcPos import util.Spans.Span import rewrites.Rewrites.patch import inlines.Inlines -import transform.SymUtils._ -import transform.ValueClasses._ -import Decorators._ +import transform.SymUtils.* +import transform.ValueClasses.* +import Decorators.* import ErrorReporting.{err, errorType} -import config.Printers.{typr, patmatch} +import config.Printers.{patmatch, saferExceptions, typr} import NameKinds.DefaultGetterName -import NameOps._ +import NameOps.* import SymDenotations.{NoCompleter, NoDenotation} import Applications.unapplyArgs import Inferencing.isFullyDefined import transform.patmat.SpaceEngine.isIrrefutable import config.Feature import config.Feature.sourceVersion -import config.SourceVersion._ +import config.SourceVersion.* import printing.Formatting.hlAsKeyword import transform.TypeUtils.* import collection.mutable -import reporting._ +import reporting.* object Checking { import tpd._ @@ -1462,6 +1461,7 @@ trait Checking { */ def checkCanThrow(tp: Type, span: Span)(using Context): Tree = if Feature.enabled(Feature.saferExceptions) && tp.isCheckedException then + saferExceptions.println(i"checking if CanThrow is in scope for type $tp") ctx.typer.implicitArgTree(defn.CanThrowClass.typeRef.appliedTo(tp), span) else EmptyTree diff --git a/compiler/src/dotty/tools/dotc/typer/Implicits.scala b/compiler/src/dotty/tools/dotc/typer/Implicits.scala index 0400d241e367..81a482697efc 100644 --- a/compiler/src/dotty/tools/dotc/typer/Implicits.scala +++ b/compiler/src/dotty/tools/dotc/typer/Implicits.scala @@ -3,36 +3,37 @@ package dotc package typer import backend.sjs.JSDefinitions -import core._ -import ast.{TreeTypeMap, untpd, tpd} -import util.Spans._ -import util.Stats.{record, monitored} -import printing.{Showable, Printer} -import printing.Texts._ -import Contexts._ -import Types._ -import Flags._ +import core.* +import ast.{TreeTypeMap, tpd, untpd} +import util.Spans.* +import util.Stats.{monitored, record} +import printing.{Printer, Showable} +import printing.Texts.* +import Contexts.* +import Types.* +import Flags.* import Mode.ImplicitsEnabled -import NameKinds.{LazyImplicitName, EvidenceParamName} -import Symbols._ -import Types._ -import Decorators._ -import Names._ -import StdNames._ -import ProtoTypes._ -import ErrorReporting._ +import NameKinds.{EvidenceParamName, LazyImplicitName} +import Symbols.* +import Types.* +import Decorators.* +import Names.* +import StdNames.* +import ProtoTypes.* +import ErrorReporting.* import Inferencing.{fullyDefinedType, isFullyDefined} import Scopes.newScope -import transform.TypeUtils._ -import Hashable._ +import transform.TypeUtils.* +import Hashable.* import util.{EqHashMap, Stats} import config.{Config, Feature} import Feature.migrateTo3 -import config.Printers.{implicits, implicitsDetailed} +import config.Printers.{implicits, implicitsDetailed, saferExceptions} + import collection.mutable -import reporting._ -import annotation.tailrec +import reporting.* +import annotation.tailrec import scala.annotation.internal.sharable import scala.annotation.threadUnsafe @@ -874,7 +875,21 @@ trait Implicits: * Return a failure as a SearchFailureType in the type of the returned tree. */ def inferImplicitArg(formal: Type, span: Span)(using Context): Tree = - inferImplicit(formal, EmptyTree, span) match + // TODO HR : Split the CanThrow capability here in case of a union type + // TODO HR : Still need to combine them all into one CanThrow clause + // If trying to infer a CanThrow capability, split it in case of a UnionType + val a = formal match + case AppliedType(base, args) if base.isRef(defn.CanThrowClass.asType) => + val t = + for arg <- args yield + arg match + case OrType(lhs, rhs) => lhs :: rhs :: Nil // Splitting or type should be done recursively + case _ => arg :: Nil + saferExceptions.println(i"${t.flatten}") + (for a <- t.flatten yield inferImplicit(defn.CanThrowClass.typeRef.appliedTo(a), EmptyTree, span)).head + case _ => + inferImplicit(formal, EmptyTree, span) + a match case SearchSuccess(arg, _, _, _) => arg case fail @ SearchFailure(failed) => if fail.isAmbiguous then failed @@ -889,6 +904,7 @@ trait Implicits: /** Search an implicit argument and report error if not found */ def implicitArgTree(formal: Type, span: Span)(using Context): Tree = { + saferExceptions.println(i"Trying to find given instance of type $formal") val arg = inferImplicitArg(formal, span) if (arg.tpe.isInstanceOf[SearchFailureType]) report.error(missingArgMsg(arg, formal, ""), ctx.source.atSpan(span)) diff --git a/tests/safer-exceptions/pos/JavaTestFile.java b/tests/safer-exceptions/pos/JavaTestFile.java index 63c826374351..4e3b60f4f94e 100644 --- a/tests/safer-exceptions/pos/JavaTestFile.java +++ b/tests/safer-exceptions/pos/JavaTestFile.java @@ -1,9 +1,10 @@ package test.saferExceptions.pos; +import scala.language.experimental.captureChecking; import java.io.IOException; public class JavaTestFile { - public static void test() throws IOException {} + public static void javatest() throws IOException {} } \ No newline at end of file diff --git a/tests/safer-exceptions/pos/t01.scala b/tests/safer-exceptions/pos/t01.scala index 9afab89e1dbc..69a1e515d108 100644 --- a/tests/safer-exceptions/pos/t01.scala +++ b/tests/safer-exceptions/pos/t01.scala @@ -7,7 +7,10 @@ import java.io.FileNotFoundException class ScalaException extends Exception +@throws[IOException | ScalaException] def test() throws ScalaException :Unit = throw new ScalaException + +def tt throws ScalaException, IOException: Unit = test() /* -- Warning: tests\safer-exceptions\pos\t01.scala:8:29 -------------------------- 8 |class ScalaException extends Exception @@ -23,7 +26,7 @@ def test() throws ScalaException :Unit = throw new ScalaException // test trait AFoo : - def foo(x : Int) throws Exception : Int + def foo(x : Int) throws Exception : Unit = tt class Foo extends AFoo : - def foo(x : Int) throws IOException, FileNotFoundException : Int = x + override def foo(x : Int) throws Exception : Unit = ??? diff --git a/tests/safer-exceptions/pos/t02.scala b/tests/safer-exceptions/pos/t02.scala index b144e395ba23..31409b705623 100644 --- a/tests/safer-exceptions/pos/t02.scala +++ b/tests/safer-exceptions/pos/t02.scala @@ -1,9 +1,16 @@ package test.saferExceptions.pos import language.experimental.saferExceptions +import language.experimental.captureChecking import test.saferExceptions.pos.JavaTestFile -def checkJavaFunction : Unit = JavaTestFile.test() +given a :CanThrow[Exception] = compiletime.erasedValue +//given b :CanThrow[Exception] = compiletime.erasedValue + +@throws[Exception] +def checkJavaFunction() throws Exception: Unit = + //given c :CanThrow[Exception] = compiletime.erasedValue + JavaTestFile.javatest() /* -- Warning: tests\safer-exceptions\pos\t02.scala:6:48 -------------------------- 6 |def checkJavaFunction : Unit = JavaTestFile.test() @@ -13,3 +20,6 @@ def checkJavaFunction : Unit = JavaTestFile.test() | Handling of Java method is yet to be implemented. */ + +def aa throws Exception: Unit = + checkJavaFunction() diff --git a/tests/safer-exceptions/pos/t03.scala b/tests/safer-exceptions/pos/t03.scala new file mode 100644 index 000000000000..43aff4383bd0 --- /dev/null +++ b/tests/safer-exceptions/pos/t03.scala @@ -0,0 +1,13 @@ +import language.experimental.saferExceptions +import java.io.* + +def multipleErrors() throws IOException, SecurityException : Int = ??? + +@main def main : Int = + try + try + multipleErrors() + catch + case _: IOException => ??? + catch + case _: SecurityException => ??? \ No newline at end of file From bb2570ed46b86075680a4cf80722be8667b97da1 Mon Sep 17 00:00:00 2001 From: Hamza REMMAL <56235032+hamzaremmal@users.noreply.github.com> Date: Wed, 4 Jan 2023 12:02:46 +0100 Subject: [PATCH 16/25] Add OrType::split to split recursively an OrType --- compiler/src/dotty/tools/dotc/core/Types.scala | 11 +++++++++++ .../src/dotty/tools/dotc/typer/Applications.scala | 7 +------ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index 29a2496ab2a7..21f0b80c2d5c 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -3489,6 +3489,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. diff --git a/compiler/src/dotty/tools/dotc/typer/Applications.scala b/compiler/src/dotty/tools/dotc/typer/Applications.scala index b37df60f8ba4..ec00dfe8f7f7 100644 --- a/compiler/src/dotty/tools/dotc/typer/Applications.scala +++ b/compiler/src/dotty/tools/dotc/typer/Applications.scala @@ -919,11 +919,6 @@ trait Applications extends Compatibility { */ def typedApply(tree: untpd.Apply, pt: Type)(using Context): Tree = { - def splitOr(tpe: Type): List[Type] = - tpe match - case OrType(lhs, rhs) => splitOr(lhs) ::: splitOr(rhs) - case _ => tpe :: Nil - def realApply(using Context): Tree = { val resultProto = tree.fun match case Select(New(tpt), _) if pt.isInstanceOf[ValueType] => @@ -944,7 +939,7 @@ trait Applications extends Compatibility { val sym: Symbol = fun1.symbol val annot = sym.annotations val throwsAnnot = annot.filter(ThrownException.unapply(_).isDefined) - val exceptions = throwsAnnot.map(ThrownException.unapply(_).get).flatMap(splitOr) + val exceptions = throwsAnnot.map(ThrownException.unapply(_).get).flatMap(OrType.split) saferExceptions.println(i"symbol $sym throws $exceptions") val capabities = for e <- exceptions yield checkCanThrow(e, tree.span) saferExceptions.println(i"fetch capabilities $capabities to satisfy conditions of $sym") From e4a769bead11f8857a73a49adc46799cc37683d2 Mon Sep 17 00:00:00 2001 From: Hamza REMMAL <56235032+hamzaremmal@users.noreply.github.com> Date: Wed, 4 Jan 2023 20:22:21 +0100 Subject: [PATCH 17/25] Implement semantics of implicit search for a CanThrow capability - still unstable --- .../dotty/tools/dotc/typer/Implicits.scala | 67 ++++++++++++++----- library/src/scala/CanThrow.scala | 5 ++ tests/safer-exceptions/pos/t03.scala | 13 ++-- 3 files changed, 65 insertions(+), 20 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/typer/Implicits.scala b/compiler/src/dotty/tools/dotc/typer/Implicits.scala index 81a482697efc..29cd1f38106f 100644 --- a/compiler/src/dotty/tools/dotc/typer/Implicits.scala +++ b/compiler/src/dotty/tools/dotc/typer/Implicits.scala @@ -871,27 +871,65 @@ trait Implicits: private var synthesizer: Synthesizer | Null = null + // =============================== Implicit Search of a CanThrow Capability ===================== + + /** + * Combain results of a CanThrow capability search + */ + def resolveCanThrow(results: List[SearchResult], acc: SearchResult | Null = null)(using Context): SearchResult = + (results, acc) match + case (Nil, _) => acc + case (x :: xs, null) => resolveCanThrow(xs, x) + case (SearchFailure(f) :: xs, SearchFailure(fcc)) => + // TODO : We should merge both failure together + resolveCanThrow(xs, acc) + case ((failure@SearchFailure(_)) :: xs, _: SearchSuccess) => + // Check the remaining of the list for other failures, success will be ignored + resolveCanThrow(xs, failure) + case ((_: SearchSuccess) :: xs, failure@SearchFailure(_)) => + // Ignore success and propagate the failure + resolveCanThrow(xs, failure) + case (SearchSuccess(arg, _, _, _) :: xs, SearchSuccess(argAcc, a, b, c)) => + // Merge both success together + // TODO HR : Still have a problem with the inference here + val capability = typed { + untpd.Apply( + untpd.Select(untpd.Ident(defn.CanThrowClass.name.toTermName), nme.apply.toTermName), + arg :: argAcc :: Nil + ) + } + // TODO HR : Resolve fix parameters a, b & c to reflect the merge + resolveCanThrow(xs, SearchSuccess(capability, a, b, c)(ctx.typerState, ctx.gadt)) + + + /** + * Find all implicit parameters for a CanThrow capability and combain them into one SearchResult + */ + def inferImplicitCanThrow(formal: AppliedType, span: Span)(using Context) : SearchResult = + val AppliedType(base, args) = formal + assert(base.isRef(defn.CanThrowClass.asType)) + val types = (for arg <- args yield OrType.split(arg)).flatten.filter(_.isCheckedException) + saferExceptions.println(i"implicit search for a CanThrow for each type in $types") + val results = + for t <- types + yield + inferImplicit(defn.CanThrowClass.typeRef.appliedTo(t), EmptyTree, span) + saferExceptions.println(i"capabilities $results") + resolveCanThrow(results) + /** Find an implicit argument for parameter `formal`. * Return a failure as a SearchFailureType in the type of the returned tree. */ def inferImplicitArg(formal: Type, span: Span)(using Context): Tree = - // TODO HR : Split the CanThrow capability here in case of a union type - // TODO HR : Still need to combine them all into one CanThrow clause // If trying to infer a CanThrow capability, split it in case of a UnionType - val a = formal match - case AppliedType(base, args) if base.isRef(defn.CanThrowClass.asType) => - val t = - for arg <- args yield - arg match - case OrType(lhs, rhs) => lhs :: rhs :: Nil // Splitting or type should be done recursively - case _ => arg :: Nil - saferExceptions.println(i"${t.flatten}") - (for a <- t.flatten yield inferImplicit(defn.CanThrowClass.typeRef.appliedTo(a), EmptyTree, span)).head - case _ => - inferImplicit(formal, EmptyTree, span) - a match + val result = formal match + case f@AppliedType(base, _) if base.isRef(defn.CanThrowClass.asType) => + inferImplicitCanThrow(f, span) + case _ => inferImplicit(formal, EmptyTree, span) + result match case SearchSuccess(arg, _, _, _) => arg case fail @ SearchFailure(failed) => + println(s"$failed") if fail.isAmbiguous then failed else if synthesizer == null then synthesizer = Synthesizer(this) @@ -904,7 +942,6 @@ trait Implicits: /** Search an implicit argument and report error if not found */ def implicitArgTree(formal: Type, span: Span)(using Context): Tree = { - saferExceptions.println(i"Trying to find given instance of type $formal") val arg = inferImplicitArg(formal, span) if (arg.tpe.isInstanceOf[SearchFailureType]) report.error(missingArgMsg(arg, formal, ""), ctx.source.atSpan(span)) diff --git a/library/src/scala/CanThrow.scala b/library/src/scala/CanThrow.scala index c7f23a393715..ff970126a234 100644 --- a/library/src/scala/CanThrow.scala +++ b/library/src/scala/CanThrow.scala @@ -10,6 +10,11 @@ import annotation.{implicitNotFound, experimental, capability} @implicitNotFound("The capability to throw exception ${E} is missing.\nThe capability can be provided by one of the following:\n - Adding a using clause `(using CanThrow[${E}])` to the definition of the enclosing method\n - Adding `throws ${E}` clause after the result type of the enclosing method\n - Wrapping this piece of code with a `try` block that catches ${E}") erased class CanThrow[-E <: Exception] +@experimental +object CanThrow: + def apply[A <: Exception, B <: Exception](ct1: CanThrow[A], ct2: CanThrow[B]): CanThrow[A | B] = + compiletime.erasedValue + @experimental object unsafeExceptions: given canThrowAny: CanThrow[Exception] = compiletime.erasedValue diff --git a/tests/safer-exceptions/pos/t03.scala b/tests/safer-exceptions/pos/t03.scala index 43aff4383bd0..ad8c7362d77b 100644 --- a/tests/safer-exceptions/pos/t03.scala +++ b/tests/safer-exceptions/pos/t03.scala @@ -1,13 +1,16 @@ import language.experimental.saferExceptions -import java.io.* -def multipleErrors() throws IOException, SecurityException : Int = ??? +class A extends Exception +class B extends Exception +class C extends Exception +def multipleErrors() throws A, B, C : Int = ??? -@main def main : Int = +def main : Int = try try multipleErrors() catch - case _: IOException => ??? + case _: A => ??? catch - case _: SecurityException => ??? \ No newline at end of file + case _: B => ??? + case _: C => ??? \ No newline at end of file From 4d43d99d1cafe2f9eef46c5d8b846243bcd8ca93 Mon Sep 17 00:00:00 2001 From: Hamza REMMAL <56235032+hamzaremmal@users.noreply.github.com> Date: Wed, 4 Jan 2023 23:32:31 +0100 Subject: [PATCH 18/25] Add check for parameterless function calls - implicit search for CanThrow capabilities --- .../dotty/tools/dotc/config/Printers.scala | 2 +- .../dotty/tools/dotc/typer/Applications.scala | 1 + .../src/dotty/tools/dotc/typer/Typer.scala | 81 ++++++++++--------- tests/safer-exceptions/t04.scala | 10 +++ 4 files changed, 57 insertions(+), 37 deletions(-) create mode 100644 tests/safer-exceptions/t04.scala diff --git a/compiler/src/dotty/tools/dotc/config/Printers.scala b/compiler/src/dotty/tools/dotc/config/Printers.scala index 57440bfffb5a..de1694cd16e5 100644 --- a/compiler/src/dotty/tools/dotc/config/Printers.scala +++ b/compiler/src/dotty/tools/dotc/config/Printers.scala @@ -49,5 +49,5 @@ object Printers { val typr = noPrinter val unapp = noPrinter val variances = noPrinter - val saferExceptions = new Printer + val saferExceptions = noPrinter } diff --git a/compiler/src/dotty/tools/dotc/typer/Applications.scala b/compiler/src/dotty/tools/dotc/typer/Applications.scala index ec00dfe8f7f7..93ba47ae0b9f 100644 --- a/compiler/src/dotty/tools/dotc/typer/Applications.scala +++ b/compiler/src/dotty/tools/dotc/typer/Applications.scala @@ -941,6 +941,7 @@ trait Applications extends Compatibility { val throwsAnnot = annot.filter(ThrownException.unapply(_).isDefined) val exceptions = throwsAnnot.map(ThrownException.unapply(_).get).flatMap(OrType.split) saferExceptions.println(i"symbol $sym throws $exceptions") + // TODO HR : Still need to add those capabilities in the capture set val capabities = for e <- exceptions yield checkCanThrow(e, tree.span) saferExceptions.println(i"fetch capabilities $capabities to satisfy conditions of $sym") if exceptions.nonEmpty then diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index 3964cd3814cc..c0f3b472117a 100644 --- a/compiler/src/dotty/tools/dotc/typer/Typer.scala +++ b/compiler/src/dotty/tools/dotc/typer/Typer.scala @@ -3,52 +3,52 @@ package dotc package typer import backend.sjs.JSDefinitions -import core._ -import ast._ -import Trees._ -import Constants._ -import StdNames._ -import Scopes._ -import Denotations._ -import ProtoTypes._ -import Contexts._ -import Symbols._ -import Types._ -import SymDenotations._ -import Annotations._ -import Names._ -import NameOps._ -import NameKinds._ -import NamerOps._ -import ContextOps._ -import Flags._ -import Decorators._ -import ErrorReporting._ -import Checking._ -import Inferencing._ +import core.* +import ast.* +import Trees.* +import Constants.* +import StdNames.* +import Scopes.* +import Denotations.* +import ProtoTypes.* +import Contexts.* +import Symbols.* +import Types.* +import SymDenotations.* +import Annotations.* +import Names.* +import NameOps.* +import NameKinds.* +import NamerOps.* +import ContextOps.* +import Flags.* +import Decorators.* +import ErrorReporting.* +import Checking.* +import Inferencing.* import Dynamic.isDynamicExpansion import EtaExpansion.etaExpand import TypeComparer.CompareResult import inlines.{Inlines, PrepareInlineable} -import util.Spans._ -import util.common._ +import util.Spans.* +import util.common.* import util.{Property, SimpleIdentityMap, SrcPos} -import Applications.{tupleComponentTypes, wrapDefs, defaultArgument} +import Applications.{defaultArgument, tupleComponentTypes, wrapDefs} import collection.mutable import annotation.tailrec -import Implicits._ +import Implicits.* import util.Stats.record -import config.Printers.{gadts, typr} +import config.Printers.{gadts, saferExceptions, typr} import config.Feature -import config.Feature.{sourceVersion, migrateTo3} -import config.SourceVersion._ +import config.Feature.{migrateTo3, sourceVersion} +import config.SourceVersion.* import rewrites.Rewrites.patch -import transform.SymUtils._ -import transform.TypeUtils._ -import reporting._ -import Nullables._ -import NullOpsDecorator._ +import transform.SymUtils.* +import transform.TypeUtils.* +import reporting.* +import Nullables.* +import NullOpsDecorator.* import cc.CheckCaptures import config.Config @@ -4103,7 +4103,16 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer tryInsertApplyOrImplicit(tree, pt, locked)(tree) // error will be reported in typedTypeApply case _ => if (ctx.mode is Mode.Type) adaptType(tree.tpe) - else adaptNoArgs(wtp) + else + val result = adaptNoArgs(wtp) + val annot = result.symbol.annotations + val throwsAnnot = annot.filter(ThrownException.unapply(_).isDefined) + val exceptions = throwsAnnot.map(ThrownException.unapply(_).get).flatMap(OrType.split) + if exceptions.nonEmpty then + saferExceptions.println(i"Checking for a CanThrow capability for these exceptions : $exceptions") + // TODO HR : Still need to add those capabilities in the capture set + val capabities = for e <- exceptions yield checkCanThrow(e, tree.span) + result } } } diff --git a/tests/safer-exceptions/t04.scala b/tests/safer-exceptions/t04.scala new file mode 100644 index 000000000000..08d4c9c20517 --- /dev/null +++ b/tests/safer-exceptions/t04.scala @@ -0,0 +1,10 @@ +import scala.language.experimental.saferExceptions + +@throws[Exception] +def test : Int = ??? + +def main : Int = + //try + test + //catch + // case _: Exception => ??? \ No newline at end of file From 995757de4d0a56faab82d2935fb23b222a1191d7 Mon Sep 17 00:00:00 2001 From: Hamza REMMAL <56235032+hamzaremmal@users.noreply.github.com> Date: Wed, 4 Jan 2023 23:44:00 +0100 Subject: [PATCH 19/25] Add an error message when parsing throws clauses without enabling the feature --- .../src/dotty/tools/dotc/parsing/Parsers.scala | 17 ++++++++++++----- tests/safer-exceptions/t04.scala | 14 +++++++------- 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index b0e073eebc95..b41de82dd33a 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -3595,13 +3595,20 @@ object Parsers { * @return A List of all the exceptions allowed to be thrown */ def throwsClauseOpt : List[Tree] = - if in.featureEnabled(Feature.saferExceptions) then - if(isIdent(nme.throws)) - in.nextToken() - val except = commaSeparated(() => toplevelTyp()) - Printers.saferExceptions.println(i"Parser will add a throws clause for the given exceptions : $except") + val startOffset = in.offset + if (isIdent(nme.throws)) + in.nextToken() + val except = commaSeparated(() => toplevelTyp()) + Printers.saferExceptions.println(i"Parser will add a throws clause for the given exceptions : $except") + if in.featureEnabled(Feature.saferExceptions) then except else + report.error( + em""" + |This syntax is part of the 'safer exceptions' project. To enable it, please add + |the following import : + | import scala.language.experimental.saferExceptions + |""".stripMargin, source.atSpan(Span(startOffset, in.offset))) Nil else Nil diff --git a/tests/safer-exceptions/t04.scala b/tests/safer-exceptions/t04.scala index 08d4c9c20517..dbe9951a90b8 100644 --- a/tests/safer-exceptions/t04.scala +++ b/tests/safer-exceptions/t04.scala @@ -1,10 +1,10 @@ -import scala.language.experimental.saferExceptions +//import scala.language.experimental.saferExceptions -@throws[Exception] -def test : Int = ??? +//@throws[Exception] +def test throws Exception : Int = ??? def main : Int = - //try - test - //catch - // case _: Exception => ??? \ No newline at end of file + try + test + catch + case _: Exception => ??? \ No newline at end of file From 9a5c8ff11fc11f6840a20a1bff065eae041dd439 Mon Sep 17 00:00:00 2001 From: Hamza REMMAL <56235032+hamzaremmal@users.noreply.github.com> Date: Wed, 4 Jan 2023 23:56:05 +0100 Subject: [PATCH 20/25] Change the semantics of the $throws infix type to follow new implementation of the 'safer exceptions' project --- compiler/src/dotty/tools/dotc/ast/Desugar.scala | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/ast/Desugar.scala b/compiler/src/dotty/tools/dotc/ast/Desugar.scala index 69aa74ddd400..5b8dad31a5f9 100644 --- a/compiler/src/dotty/tools/dotc/ast/Desugar.scala +++ b/compiler/src/dotty/tools/dotc/ast/Desugar.scala @@ -1321,13 +1321,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) From db484bbfb01021f925fab2c6c3c7173179321f78 Mon Sep 17 00:00:00 2001 From: Hamza REMMAL <56235032+hamzaremmal@users.noreply.github.com> Date: Thu, 5 Jan 2023 00:03:58 +0100 Subject: [PATCH 21/25] Change implicitNotFound message for the CanThrow class to drop the contextual parameter list --- library/src/scala/CanThrow.scala | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/library/src/scala/CanThrow.scala b/library/src/scala/CanThrow.scala index ff970126a234..2ea287e8f2f2 100644 --- a/library/src/scala/CanThrow.scala +++ b/library/src/scala/CanThrow.scala @@ -2,12 +2,13 @@ package scala import language.experimental.erasedDefinitions import annotation.{implicitNotFound, experimental, capability} -/** A capability class that allows to throw exception `E`. When used with the +/** + * A capability class that allows to throw exception `E`. When used with the * experimental.saferExceptions feature, a `throw Ex()` expression will require * a given of class `CanThrow[Ex]` to be available. */ @experimental @capability -@implicitNotFound("The capability to throw exception ${E} is missing.\nThe capability can be provided by one of the following:\n - Adding a using clause `(using CanThrow[${E}])` to the definition of the enclosing method\n - Adding `throws ${E}` clause after the result type of the enclosing method\n - Wrapping this piece of code with a `try` block that catches ${E}") +@implicitNotFound("The capability to throw exception of type ${E} is missing.\nThe capability can be provided by one of the following:\n - Adding `throws ${E}` clause after the parameter list of the enclosing method\n - Adding `throws ${E}` clause after the result type of the enclosing method\n - Wrapping this piece of code with a `try` block that catches ${E}") erased class CanThrow[-E <: Exception] @experimental From 073206e47a09f62ea99f5b0d7b80e7f4154ed630 Mon Sep 17 00:00:00 2001 From: Hamza REMMAL <56235032+hamzaremmal@users.noreply.github.com> Date: Thu, 19 Jan 2023 15:30:11 +0100 Subject: [PATCH 22/25] Fix error messages in Parsers & Applications --- compiler/src/dotty/tools/dotc/parsing/Parsers.scala | 2 +- compiler/src/dotty/tools/dotc/typer/Applications.scala | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index b41de82dd33a..4595e8499ee2 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -3604,7 +3604,7 @@ object Parsers { except else report.error( - em""" + i""" |This syntax is part of the 'safer exceptions' project. To enable it, please add |the following import : | import scala.language.experimental.saferExceptions diff --git a/compiler/src/dotty/tools/dotc/typer/Applications.scala b/compiler/src/dotty/tools/dotc/typer/Applications.scala index 93ba47ae0b9f..24e6e887533b 100644 --- a/compiler/src/dotty/tools/dotc/typer/Applications.scala +++ b/compiler/src/dotty/tools/dotc/typer/Applications.scala @@ -946,7 +946,7 @@ trait Applications extends Compatibility { saferExceptions.println(i"fetch capabilities $capabities to satisfy conditions of $sym") if exceptions.nonEmpty then report.warning( - em""" A function was called (${sym.name}) in a context where safer exceptions is enabled. + i""" A function was called (${sym.name}) in a context where safer exceptions is enabled. | This function throws an exception. | ${exceptions zip capabities} |""".stripMargin, tree.srcPos) From 6da1b43b8a202bdeb97028c0a1ddf6092a26f575 Mon Sep 17 00:00:00 2001 From: Hamza REMMAL <56235032+hamzaremmal@users.noreply.github.com> Date: Thu, 19 Jan 2023 15:44:43 +0100 Subject: [PATCH 23/25] Resolve type inference problem in Implicits --- compiler/src/dotty/tools/dotc/typer/Implicits.scala | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/typer/Implicits.scala b/compiler/src/dotty/tools/dotc/typer/Implicits.scala index d8c49ab945a5..5b2f39940af9 100644 --- a/compiler/src/dotty/tools/dotc/typer/Implicits.scala +++ b/compiler/src/dotty/tools/dotc/typer/Implicits.scala @@ -884,17 +884,15 @@ trait Implicits: case ((_: SearchSuccess) :: xs, failure@SearchFailure(_)) => // Ignore success and propagate the failure resolveCanThrow(xs, failure) - case (SearchSuccess(arg, _, _, _) :: xs, SearchSuccess(argAcc, a, b, c)) => + case (SearchSuccess(arg, _, l1, e1) :: xs, SearchSuccess(argAcc, _, l2, e2)) => // Merge both success together // TODO HR : Still have a problem with the inference here - val capability = typed { - untpd.Apply( - untpd.Select(untpd.Ident(defn.CanThrowClass.name.toTermName), nme.apply.toTermName), + val capability = + tpd.Apply( + tpd.Select(tpd.Ident(defn.CanThrowClass.companionModule.termRef), nme.apply.toTermName), arg :: argAcc :: Nil ) - } - // TODO HR : Resolve fix parameters a, b & c to reflect the merge - resolveCanThrow(xs, SearchSuccess(capability, a, b, c)(ctx.typerState, ctx.gadt)) + resolveCanThrow(xs, SearchSuccess(capability, capability.symbol.termRef, l1 min l2, e1 || e2)(ctx.typerState, ctx.gadt)) /** @@ -924,7 +922,6 @@ trait Implicits: result match case SearchSuccess(arg, _, _, _) => arg case fail @ SearchFailure(failed) => - println(s"$failed") if fail.isAmbiguous then failed else if synthesizer == null then synthesizer = Synthesizer(this) From 36b9aa51f8c80c213ef3e725d6ef0eb53d119988 Mon Sep 17 00:00:00 2001 From: Hamza REMMAL <56235032+hamzaremmal@users.noreply.github.com> Date: Thu, 19 Jan 2023 15:51:19 +0100 Subject: [PATCH 24/25] Restructure resolveCanThrow method to compile with scala 3.3.0 --- .../dotty/tools/dotc/typer/Implicits.scala | 46 ++++++++++--------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/typer/Implicits.scala b/compiler/src/dotty/tools/dotc/typer/Implicits.scala index 5b2f39940af9..0c4937a57bef 100644 --- a/compiler/src/dotty/tools/dotc/typer/Implicits.scala +++ b/compiler/src/dotty/tools/dotc/typer/Implicits.scala @@ -871,28 +871,30 @@ trait Implicits: /** * Combain results of a CanThrow capability search */ - def resolveCanThrow(results: List[SearchResult], acc: SearchResult | Null = null)(using Context): SearchResult = - (results, acc) match - case (Nil, _) => acc - case (x :: xs, null) => resolveCanThrow(xs, x) - case (SearchFailure(f) :: xs, SearchFailure(fcc)) => - // TODO : We should merge both failure together - resolveCanThrow(xs, acc) - case ((failure@SearchFailure(_)) :: xs, _: SearchSuccess) => - // Check the remaining of the list for other failures, success will be ignored - resolveCanThrow(xs, failure) - case ((_: SearchSuccess) :: xs, failure@SearchFailure(_)) => - // Ignore success and propagate the failure - resolveCanThrow(xs, failure) - case (SearchSuccess(arg, _, l1, e1) :: xs, SearchSuccess(argAcc, _, l2, e2)) => - // Merge both success together - // TODO HR : Still have a problem with the inference here - val capability = - tpd.Apply( - tpd.Select(tpd.Ident(defn.CanThrowClass.companionModule.termRef), nme.apply.toTermName), - arg :: argAcc :: Nil - ) - resolveCanThrow(xs, SearchSuccess(capability, capability.symbol.termRef, l1 min l2, e1 || e2)(ctx.typerState, ctx.gadt)) + def resolveCanThrow(results: List[SearchResult])(using Context): SearchResult = + def merge(l: List[SearchResult], acc: SearchResult)(using Context): SearchResult = + (l, acc) match + case (Nil, _) => acc + case (SearchFailure(f) :: xs, SearchFailure(fcc)) => + // TODO : We should merge both failure together + merge(xs, acc) + case ((failure@SearchFailure(_)) :: xs, _: SearchSuccess) => + // Check the remaining of the list for other failures, success will be ignored + merge(xs, failure) + case ((_: SearchSuccess) :: xs, failure@SearchFailure(_)) => + // Ignore success and propagate the failure + merge(xs, failure) + case (SearchSuccess(arg, _, l1, e1) :: xs, SearchSuccess(argAcc, _, l2, e2)) => + // Merge both success together + val capability = + tpd.Apply( + tpd.Select(tpd.Ident(defn.CanThrowClass.companionModule.termRef), nme.apply.toTermName), + arg :: argAcc :: Nil + ) + merge(xs, SearchSuccess(capability, capability.symbol.termRef, l1 min l2, e1 || e2)(ctx.typerState, ctx.gadt)) + + assert(results.nonEmpty) // We cannot merge an empty list of SearchResult + merge(results.tail, results.head) /** From f2f330af55a770c0b81c089e80fc169b958d425f Mon Sep 17 00:00:00 2001 From: Hamza REMMAL <56235032+hamzaremmal@users.noreply.github.com> Date: Thu, 19 Jan 2023 16:32:21 +0100 Subject: [PATCH 25/25] Add error message when using throws clauses without return type --- .../src/dotty/tools/dotc/ast/Desugar.scala | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/ast/Desugar.scala b/compiler/src/dotty/tools/dotc/ast/Desugar.scala index df777b609a5a..22e732921a5c 100644 --- a/compiler/src/dotty/tools/dotc/ast/Desugar.scala +++ b/compiler/src/dotty/tools/dotc/ast/Desugar.scala @@ -1777,11 +1777,22 @@ object desugar { val desugared = tree match { case ThrowsReturn(exceptions, rteTpe) => - 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 clased desugared to $fn") - fn + 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) =>