diff --git a/compiler/src/dotty/tools/dotc/ast/Desugar.scala b/compiler/src/dotty/tools/dotc/ast/Desugar.scala index 5326361ada98..22e732921a5c 100644 --- a/compiler/src/dotty/tools/dotc/ast/Desugar.scala +++ b/compiler/src/dotty/tools/dotc/ast/Desugar.scala @@ -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) @@ -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) => diff --git a/compiler/src/dotty/tools/dotc/ast/Trees.scala b/compiler/src/dotty/tools/dotc/ast/Trees.scala index 1053e7c86780..93bdc4ece347 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 8a6ba48d22c5..914f87680b58 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 @@ -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) } diff --git a/compiler/src/dotty/tools/dotc/config/Printers.scala b/compiler/src/dotty/tools/dotc/config/Printers.scala index 63d616e1ce3d..06fc4b992b22 100644 --- a/compiler/src/dotty/tools/dotc/config/Printers.scala +++ b/compiler/src/dotty/tools/dotc/config/Printers.scala @@ -50,4 +50,5 @@ object Printers { val typr = noPrinter val unapp = noPrinter val variances = noPrinter + val saferExceptions = noPrinter } 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 diff --git a/compiler/src/dotty/tools/dotc/core/Types.scala b/compiler/src/dotty/tools/dotc/core/Types.scala index 956236b955db..b333a2d64eb2 100644 --- a/compiler/src/dotty/tools/dotc/core/Types.scala +++ b/compiler/src/dotty/tools/dotc/core/Types.scala @@ -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. diff --git a/compiler/src/dotty/tools/dotc/parsing/JavaParsers.scala b/compiler/src/dotty/tools/dotc/parsing/JavaParsers.scala index daeebcbcc17c..025461ccd8b6 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 { @@ -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() @@ -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)) } } } @@ -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) @@ -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 diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index ff5e95f3aa03..b48d54add1e8 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 { @@ -3554,9 +3555,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 = @@ -3573,13 +3575,43 @@ 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] = + 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( + i""" + |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 + /** ConstrExpr ::= SelfInvocation * | `{' SelfInvocation {semi BlockStat} `}' */ diff --git a/compiler/src/dotty/tools/dotc/typer/Applications.scala b/compiler/src/dotty/tools/dotc/typer/Applications.scala index 3482f80389d8..56251ab9fefb 100644 --- a/compiler/src/dotty/tools/dotc/typer/Applications.scala +++ b/compiler/src/dotty/tools/dotc/typer/Applications.scala @@ -2,39 +2,42 @@ 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._, NullOpsDecorator.* +import ProtoTypes.* +import Inferencing.* +import reporting.* +import transform.TypeUtils.* +import transform.SymUtils.* +import Nullables.* +import Checking.* +import NullOpsDecorator.* 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 { @@ -909,6 +912,15 @@ 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. @@ -928,11 +940,25 @@ 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) - + 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).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 + 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 = diff --git a/compiler/src/dotty/tools/dotc/typer/Checking.scala b/compiler/src/dotty/tools/dotc/typer/Checking.scala index a1ee2c539622..dc79f6d20f33 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._ @@ -1464,6 +1463,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 3e14292419a5..0c4937a57bef 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 @@ -865,11 +866,62 @@ 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])(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) + + + /** + * 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 = - inferImplicit(formal, EmptyTree, span) match + // If trying to infer a CanThrow capability, split it in case of a UnionType + 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) => if fail.isAmbiguous then failed diff --git a/compiler/src/dotty/tools/dotc/typer/Typer.scala b/compiler/src/dotty/tools/dotc/typer/Typer.scala index e65eba4d5dd8..9813482c98bb 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 @@ -2792,6 +2792,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) @@ -4120,7 +4122,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/library/src/scala/CanThrow.scala b/library/src/scala/CanThrow.scala index c7f23a393715..2ea287e8f2f2 100644 --- a/library/src/scala/CanThrow.scala +++ b/library/src/scala/CanThrow.scala @@ -2,14 +2,20 @@ 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 +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/neg/t01.scala b/tests/safer-exceptions/neg/t01.scala new file mode 100644 index 000000000000..d48aa787f16e --- /dev/null +++ b/tests/safer-exceptions/neg/t01.scala @@ -0,0 +1,9 @@ +import java.io.IOException + +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/JavaTestFile.java b/tests/safer-exceptions/pos/JavaTestFile.java new file mode 100644 index 000000000000..4e3b60f4f94e --- /dev/null +++ b/tests/safer-exceptions/pos/JavaTestFile.java @@ -0,0 +1,10 @@ +package test.saferExceptions.pos; +import scala.language.experimental.captureChecking; + +import java.io.IOException; + +public class JavaTestFile { + + 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 new file mode 100644 index 000000000000..69a1e515d108 --- /dev/null +++ b/tests/safer-exceptions/pos/t01.scala @@ -0,0 +1,32 @@ +package test.saferExceptions.pos + +import language.experimental.saferExceptions +import java.io.IOException +import java.io.FileInputStream +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 + | ^^^^^^^^^ + | 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 ? +//@main def a(args : Array[String]) throws IOException : Unit = +// test + +trait AFoo : + def foo(x : Int) throws Exception : Unit = tt + +class Foo extends AFoo : + override def foo(x : Int) throws Exception : Unit = ??? diff --git a/tests/safer-exceptions/pos/t02.scala b/tests/safer-exceptions/pos/t02.scala new file mode 100644 index 000000000000..31409b705623 --- /dev/null +++ b/tests/safer-exceptions/pos/t02.scala @@ -0,0 +1,25 @@ +package test.saferExceptions.pos + +import language.experimental.saferExceptions +import language.experimental.captureChecking +import test.saferExceptions.pos.JavaTestFile + +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() + | ^^^^^^^^^^^^^^^^^^^ + | 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. + +*/ + +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..ad8c7362d77b --- /dev/null +++ b/tests/safer-exceptions/pos/t03.scala @@ -0,0 +1,16 @@ +import language.experimental.saferExceptions + +class A extends Exception +class B extends Exception +class C extends Exception +def multipleErrors() throws A, B, C : Int = ??? + +def main : Int = + try + try + multipleErrors() + catch + case _: A => ??? + catch + case _: B => ??? + case _: C => ??? \ No newline at end of file diff --git a/tests/safer-exceptions/t04.scala b/tests/safer-exceptions/t04.scala new file mode 100644 index 000000000000..dbe9951a90b8 --- /dev/null +++ b/tests/safer-exceptions/t04.scala @@ -0,0 +1,10 @@ +//import scala.language.experimental.saferExceptions + +//@throws[Exception] +def test throws Exception : Int = ??? + +def main : Int = + try + test + catch + case _: Exception => ??? \ No newline at end of file