diff --git a/compiler/src/dotty/tools/dotc/ast/tpd.scala b/compiler/src/dotty/tools/dotc/ast/tpd.scala index 1827638ba9ad..3a47f9768958 100644 --- a/compiler/src/dotty/tools/dotc/ast/tpd.scala +++ b/compiler/src/dotty/tools/dotc/ast/tpd.scala @@ -868,6 +868,10 @@ object tpd extends Trees.Instance[Type] with TypedTreeInfo { def appliedToArgs(args: List[Tree])(implicit ctx: Context): Apply = Apply(tree, args) + /** An applied node that accepts only varargs as arguments */ + def appliedToVarargs(args: List[Tree], tpt: Tree)(given Context): Tree = + appliedTo(repeated(args, tpt)) + /** The current tree applied to given argument lists: * `tree (argss(0)) ... (argss(argss.length -1))` */ @@ -885,6 +889,10 @@ object tpd extends Trees.Instance[Type] with TypedTreeInfo { def appliedToTypes(targs: List[Type])(implicit ctx: Context): Tree = appliedToTypeTrees(targs map (TypeTree(_))) + /** The current tree applied to given type argument: `tree[targ]` */ + def appliedToTypeTree(targ: Tree)(implicit ctx: Context): Tree = + appliedToTypeTrees(targ :: Nil) + /** The current tree applied to given type argument list: `tree[targs(0), ..., targs(targs.length - 1)]` */ def appliedToTypeTrees(targs: List[Tree])(implicit ctx: Context): Tree = if (targs.isEmpty) tree else TypeApply(tree, targs) @@ -1357,5 +1365,24 @@ object tpd extends Trees.Instance[Type] with TypedTreeInfo { val targs = atp.argTypes tpd.applyOverloaded(New(atp.typeConstructor), nme.CONSTRUCTOR, args, targs, atp) } -} + /** Convert a list of trees to a vararg-compatible tree. + * Used to make arguments for methods that accept varargs. + */ + def repeated(trees: List[Tree], tpt: Tree)(given ctx: Context): Tree = + ctx.typeAssigner.arrayToRepeated(JavaSeqLiteral(trees, tpt)) + + /** Create a tree representing a list containing all + * the elements of the argument list. A "list of tree to + * tree of list" conversion. + * + * @param trees the elements the list represented by + * the resulting tree should contain. + * @param tpe the type of the elements of the resulting list. + * + */ + def mkList(trees: List[Tree], tpe: Tree)(given Context): Tree = + ref(defn.ListModule).select(nme.apply) + .appliedToTypeTree(tpe) + .appliedToVarargs(trees, tpe) +} diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index df5f6194d8ed..0b370abeca16 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -225,7 +225,14 @@ class Definitions { @tu lazy val Compiletime_code : Symbol = CompiletimePackageObject.requiredMethod("code") @tu lazy val Compiletime_summonFrom : Symbol = CompiletimePackageObject.requiredMethod("summonFrom") @tu lazy val CompiletimeTestingPackageObject: Symbol = ctx.requiredModule("scala.compiletime.testing.package") - @tu lazy val CompiletimeTesting_typeChecks : Symbol = CompiletimeTestingPackageObject.requiredMethod("typeChecks") + @tu lazy val CompiletimeTesting_typeChecks: Symbol = CompiletimeTestingPackageObject.requiredMethod("typeChecks") + @tu lazy val CompiletimeTesting_typeCheckErrors: Symbol = CompiletimeTestingPackageObject.requiredMethod("typeCheckErrors") + @tu lazy val CompiletimeTesting_ErrorClass: ClassSymbol = ctx.requiredClass("scala.compiletime.testing.Error") + @tu lazy val CompiletimeTesting_Error: Symbol = ctx.requiredModule("scala.compiletime.testing.Error") + @tu lazy val CompiletimeTesting_Error_apply = CompiletimeTesting_Error.requiredMethod(nme.apply) + @tu lazy val CompiletimeTesting_ErrorKind: Symbol = ctx.requiredModule("scala.compiletime.testing.ErrorKind") + @tu lazy val CompiletimeTesting_ErrorKind_Parser: Symbol = CompiletimeTesting_ErrorKind.requiredMethod("Parser") + @tu lazy val CompiletimeTesting_ErrorKind_Typer: Symbol = CompiletimeTesting_ErrorKind.requiredMethod("Typer") /** The `scalaShadowing` package is used to safely modify classes and * objects in scala so that they can be used from dotty. They will @@ -389,6 +396,7 @@ class Definitions { methodNames.map(getWrapVarargsArrayModule.requiredMethod(_)) }) + @tu lazy val ListModule: Symbol = ctx.requiredModule("scala.collection.immutable.List") @tu lazy val NilModule: Symbol = ctx.requiredModule("scala.collection.immutable.Nil") @tu lazy val SingletonClass: ClassSymbol = diff --git a/compiler/src/dotty/tools/dotc/typer/Inliner.scala b/compiler/src/dotty/tools/dotc/typer/Inliner.scala index 632b5be4ae53..c3ed700b059b 100644 --- a/compiler/src/dotty/tools/dotc/typer/Inliner.scala +++ b/compiler/src/dotty/tools/dotc/typer/Inliner.scala @@ -66,7 +66,9 @@ object Inliner { * and body that replace it. */ def inlineCall(tree: Tree)(implicit ctx: Context): Tree = { - if (tree.symbol == defn.CompiletimeTesting_typeChecks) return Intrinsics.typeChecks(tree) + if tree.symbol.denot != SymDenotations.NoDenotation && tree.symbol.owner.companionModule == defn.CompiletimeTestingPackageObject + if (tree.symbol == defn.CompiletimeTesting_typeChecks) return Intrinsics.typeChecks(tree) + if (tree.symbol == defn.CompiletimeTesting_typeCheckErrors) return Intrinsics.typeCheckErrors(tree) /** Set the position of all trees logically contained in the expansion of * inlined call `call` to the position of `call`. This transform is necessary @@ -193,10 +195,12 @@ object Inliner { } object Intrinsics { + import dotty.tools.dotc.reporting.diagnostic.messages.Error + private enum ErrorKind + case Parser, Typer - /** Expand call to scala.compiletime.testing.typeChecks */ - def typeChecks(tree: Tree)(implicit ctx: Context): Tree = { - assert(tree.symbol == defn.CompiletimeTesting_typeChecks) + private def compileForErrors(tree: Tree, stopAfterParser: Boolean)(given ctx: Context): List[(ErrorKind, Error)] = + assert(tree.symbol == defn.CompiletimeTesting_typeChecks || tree.symbol == defn.CompiletimeTesting_typeCheckErrors) def stripTyped(t: Tree): Tree = t match { case Typed(t2, _) => stripTyped(t2) case _ => t @@ -205,20 +209,49 @@ object Inliner { val Apply(_, codeArg :: Nil) = tree ConstFold(stripTyped(codeArg.underlyingArgument)).tpe.widenTermRefExpr match { case ConstantType(Constant(code: String)) => - val ctx2 = ctx.fresh.setNewTyperState().setTyper(new Typer) - val tree2 = new Parser(SourceFile.virtual("tasty-reflect", code))(ctx2).block() - val res = - if (ctx2.reporter.hasErrors) false - else { - ctx2.typer.typed(tree2)(ctx2) - !ctx2.reporter.hasErrors - } - Literal(Constant(res)) + val source2 = SourceFile.virtual("tasty-reflect", code) + val ctx2 = ctx.fresh.setNewTyperState().setTyper(new Typer).setSource(source2) + val tree2 = new Parser(source2)(ctx2).block() + val res = collection.mutable.ListBuffer.empty[(ErrorKind, Error)] + + val parseErrors = ctx2.reporter.allErrors.toList + res ++= parseErrors.map(e => ErrorKind.Parser -> e) + if !stopAfterParser || res.isEmpty + ctx2.typer.typed(tree2)(ctx2) + val typerErrors = ctx2.reporter.allErrors.filterNot(parseErrors.contains) + res ++= typerErrors.map(e => ErrorKind.Typer -> e) + res.toList case t => assert(ctx.reporter.hasErrors) // at least: argument to inline parameter must be a known value - EmptyTree + Nil } - } + + private def packError(kind: ErrorKind, error: Error)(given Context): Tree = + def lit(x: Any) = Literal(Constant(x)) + val constructor: Tree = ref(defn.CompiletimeTesting_Error_apply) + val parserErrorKind: Tree = ref(defn.CompiletimeTesting_ErrorKind_Parser) + val typerErrorKind: Tree = ref(defn.CompiletimeTesting_ErrorKind_Typer) + + constructor.appliedTo( + lit(error.message), + lit(error.pos.lineContent.reverse.dropWhile("\n ".contains).reverse), + lit(error.pos.column), + if kind == ErrorKind.Parser then parserErrorKind else typerErrorKind) + + private def packErrors(errors: List[(ErrorKind, Error)])(given Context): Tree = + val individualErrors: List[Tree] = errors.map(packError) + val errorTpt = ref(defn.CompiletimeTesting_ErrorClass) + mkList(individualErrors, errorTpt) + + /** Expand call to scala.compiletime.testing.typeChecks */ + def typeChecks(tree: Tree)(given Context): Tree = + val errors = compileForErrors(tree, true) + Literal(Constant(errors.isEmpty)) + + /** Expand call to scala.compiletime.testing.typeCheckErrors */ + def typeCheckErrors(tree: Tree)(given Context): Tree = + val errors = compileForErrors(tree, false) + packErrors(errors) } } diff --git a/compiler/src/dotty/tools/dotc/typer/QuotesAndSplices.scala b/compiler/src/dotty/tools/dotc/typer/QuotesAndSplices.scala index 0685a8b7d488..b404d6a92d37 100644 --- a/compiler/src/dotty/tools/dotc/typer/QuotesAndSplices.scala +++ b/compiler/src/dotty/tools/dotc/typer/QuotesAndSplices.scala @@ -368,7 +368,7 @@ trait QuotesAndSplices { val quoteClass = if (tree.quoted.isTerm) defn.QuotedExprClass else defn.QuotedTypeClass val quotedPattern = if (tree.quoted.isTerm) ref(defn.InternalQuoted_exprQuote.termRef).appliedToType(defn.AnyType).appliedTo(shape).select(nme.apply).appliedTo(qctx) - else ref(defn.InternalQuoted_typeQuote.termRef).appliedToTypeTrees(shape :: Nil) + else ref(defn.InternalQuoted_typeQuote.termRef).appliedToTypeTree(shape) UnApply( fun = ref(unapplySym.termRef).appliedToTypeTrees(typeBindingsTuple :: TypeTree(patType) :: Nil), implicits = quotedPattern :: Literal(Constant(typeBindings.nonEmpty)) :: qctx :: Nil, diff --git a/library/src/scala/compiletime/testing/Error.scala b/library/src/scala/compiletime/testing/Error.scala new file mode 100644 index 000000000000..fc6ca0bcaabc --- /dev/null +++ b/library/src/scala/compiletime/testing/Error.scala @@ -0,0 +1,12 @@ +package scala.compiletime.testing + +/** Represents a compile-time error. + * + * @see scala.compiletime.testing.typeCheckErrors + * + * IMPORTANT: No stability guarantees are provided on the format of these + * errors. This means the format and the API may change from + * version to version. This API is to be used for testing purposes + * only. + */ +final case class Error(message: String, lineContent: String, column: Int, kind: ErrorKind) diff --git a/library/src/scala/compiletime/testing/ErrorKind.scala b/library/src/scala/compiletime/testing/ErrorKind.scala new file mode 100644 index 000000000000..dadf21f2364e --- /dev/null +++ b/library/src/scala/compiletime/testing/ErrorKind.scala @@ -0,0 +1,7 @@ +package scala.compiletime.testing + +/** An error can be either a parse-time or a typecheck-time */ +sealed trait ErrorKind // TODO make this enum, so far not doable because ScalaJS compilation fails on it +object ErrorKind + case object Parser extends ErrorKind + case object Typer extends ErrorKind diff --git a/library/src/scala/compiletime/testing/package.scala b/library/src/scala/compiletime/testing/package.scala index 42f92b62bb4c..690ecb1574d9 100644 --- a/library/src/scala/compiletime/testing/package.scala +++ b/library/src/scala/compiletime/testing/package.scala @@ -1,7 +1,5 @@ package scala.compiletime -import scala.quoted._ - package object testing { /** Whether the code type checks in the current context? @@ -15,4 +13,19 @@ package object testing { inline def typeChecks(inline code: String): Boolean = error("`typeChecks` was not checked by the compiler") + /** Whether the code type checks in the current context? If not, + * returns a list of errors encountered on compilation. + * IMPORTANT: No stability guarantees are provided on the format of these + * errors. This means the format and the API may change from + * version to version. This API is to be used for testing purposes + * only. + * + * @param code The code to be type checked + * + * @return a list of errors encountered during parsing and typechecking. + * + * The code should be a sequence of expressions or statements that may appear in a block. + */ + inline def typeCheckErrors(inline code: String): List[Error] = + error("`typeCheckErrors` was not checked by the compiler") } diff --git a/tests/run-bootstrapped/typeCheckErrors.check b/tests/run-bootstrapped/typeCheckErrors.check new file mode 100644 index 000000000000..f3fa8e02154c --- /dev/null +++ b/tests/run-bootstrapped/typeCheckErrors.check @@ -0,0 +1,2 @@ +Error(Not found: abc, 1 + abc,16,Typer) +Error(Not found: abc, 1 + abc,16,Typer) diff --git a/tests/run-bootstrapped/typeCheckErrors.scala b/tests/run-bootstrapped/typeCheckErrors.scala new file mode 100644 index 000000000000..492ae626fb73 --- /dev/null +++ b/tests/run-bootstrapped/typeCheckErrors.scala @@ -0,0 +1,10 @@ +@main def Test = + scala.compiletime.testing.typeCheckErrors(""" + + + + 1 + abc + + + + """).foreach(println) \ No newline at end of file