diff --git a/compiler/src/dotty/tools/dotc/core/Definitions.scala b/compiler/src/dotty/tools/dotc/core/Definitions.scala index 78199a2659be..07c1fb8d565a 100644 --- a/compiler/src/dotty/tools/dotc/core/Definitions.scala +++ b/compiler/src/dotty/tools/dotc/core/Definitions.scala @@ -234,6 +234,8 @@ class Definitions { def Compiletime_constValue(implicit ctx: Context): Symbol = Compiletime_constValueR.symbol @threadUnsafe lazy val Compiletime_constValueOptR: TermRef = CompiletimePackageObjectRef.symbol.requiredMethodRef("constValueOpt") def Compiletime_constValueOpt(implicit ctx: Context): Symbol = Compiletime_constValueOptR.symbol + @threadUnsafe lazy val Compiletime_codeR: TermRef = CompiletimePackageObjectRef.symbol.requiredMethodRef("code") + def Compiletime_code(implicit ctx: Context): Symbol = Compiletime_codeR.symbol /** The `scalaShadowing` package is used to safely modify classes and * objects in scala so that they can be used from dotty. They will diff --git a/compiler/src/dotty/tools/dotc/typer/Inliner.scala b/compiler/src/dotty/tools/dotc/typer/Inliner.scala index 6fe1a26327f0..6e61baaad096 100644 --- a/compiler/src/dotty/tools/dotc/typer/Inliner.scala +++ b/compiler/src/dotty/tools/dotc/typer/Inliner.scala @@ -472,7 +472,7 @@ class Inliner(call: tpd.Tree, rhsToInline: tpd.Tree)(implicit ctx: Context) { val expansion = inliner.transform(rhsToInline) def issueError() = callValueArgss match { - case (msgArg :: rest) :: Nil => + case (msgArg :: Nil) :: Nil => msgArg.tpe match { case ConstantType(Constant(msg: String)) => // Usually `error` is called from within a rewrite method. In this @@ -482,16 +482,7 @@ class Inliner(call: tpd.Tree, rhsToInline: tpd.Tree)(implicit ctx: Context) { val callToReport = if (enclosingInlineds.nonEmpty) enclosingInlineds.last else call val ctxToReport = ctx.outersIterator.dropWhile(enclosingInlineds(_).nonEmpty).next def issueInCtx(implicit ctx: Context) = { - def decompose(arg: Tree): String = arg match { - case Typed(arg, _) => decompose(arg) - case SeqLiteral(elems, _) => elems.map(decompose).mkString(", ") - case arg => - arg.tpe.widenTermRefExpr match { - case ConstantType(Constant(c)) => c.toString - case _ => arg.show - } - } - ctx.error(s"$msg${rest.map(decompose).mkString(", ")}", callToReport.sourcePos) + ctx.error(msg, callToReport.sourcePos) } issueInCtx(ctxToReport) case _ => @@ -499,6 +490,41 @@ class Inliner(call: tpd.Tree, rhsToInline: tpd.Tree)(implicit ctx: Context) { case _ => } + def issueCode()(implicit ctx: Context): Literal = { + def decompose(arg: Tree): String = arg match { + case Typed(arg, _) => decompose(arg) + case SeqLiteral(elems, _) => elems.map(decompose).mkString(", ") + case Block(Nil, expr) => decompose(expr) + case Inlined(_, Nil, expr) => decompose(expr) + case arg => + arg.tpe.widenTermRefExpr match { + case ConstantType(Constant(c)) => c.toString + case _ => arg.show + } + } + + def malformedString(): String = { + ctx.error("Malformed part `code` string interpolator", call.sourcePos) + "" + } + + callValueArgss match { + case List(List(Apply(_,List(Typed(SeqLiteral(Literal(headConst) :: parts,_),_)))), List(Typed(SeqLiteral(interpolatedParts,_),_))) + if parts.size == interpolatedParts.size => + val constantParts = parts.map { + case Literal(const) => const.stringValue + case _ => malformedString() + } + val decomposedInterpolations = interpolatedParts.map(decompose) + val constantString = decomposedInterpolations.zip(constantParts) + .foldLeft(headConst.stringValue) { case (acc, (p1, p2)) => acc + p1 + p2 } + + Literal(Constant(constantString)).withSpan(call.span) + case _ => + Literal(Constant(malformedString())) + } + } + trace(i"inlining $call", inlining, show = true) { // The normalized bindings collected in `bindingsBuf` @@ -522,9 +548,13 @@ class Inliner(call: tpd.Tree, rhsToInline: tpd.Tree)(implicit ctx: Context) { if (inlinedMethod == defn.Compiletime_error) issueError() - // Take care that only argument bindings go into `bindings`, since positions are - // different for bindings from arguments and bindings from body. - tpd.Inlined(call, finalBindings, finalExpansion) + if (inlinedMethod == defn.Compiletime_code) { + issueCode()(ctx.fresh.setSetting(ctx.settings.color, "never")) + } else { + // Take care that only argument bindings go into `bindings`, since positions are + // different for bindings from arguments and bindings from body. + tpd.Inlined(call, finalBindings, finalExpansion) + } } } diff --git a/docs/docs/reference/metaprogramming/inline.md b/docs/docs/reference/metaprogramming/inline.md index ead86a97be20..89a83691a624 100644 --- a/docs/docs/reference/metaprogramming/inline.md +++ b/docs/docs/reference/metaprogramming/inline.md @@ -383,12 +383,28 @@ The `error` method is used to produce user-defined compile errors during inline It has the following signature: ```scala -inline def error(inline msg: String, objs: Any*): Nothing +inline def error(inline msg: String): Nothing ``` If an inline expansion results in a call `error(msgStr)` the compiler produces an error message containing the given `msgStr`. +```scala +inline def fail() = { + error("failed for a reason") +} +fail() // error: failed for a reason +``` + +or + +```scala +inline def fail(p1: => Any) = { + error(code"failed on: $p1") +} +fail(indentity("foo")) // error: failed on: indentity("foo") +``` + ## Implicit Matches It is foreseen that many areas of typelevel programming can be done with rewrite diff --git a/library/src-3.x/scala/compiletime/package.scala b/library/src-3.x/scala/compiletime/package.scala index da7574d0969a..7152c2b110bd 100644 --- a/library/src-3.x/scala/compiletime/package.scala +++ b/library/src-3.x/scala/compiletime/package.scala @@ -4,7 +4,34 @@ package object compiletime { erased def erasedValue[T]: T = ??? - inline def error(inline msg: String, objs: Any*): Nothing = ??? + /** The error method is used to produce user-defined compile errors during inline expansion. + * If an inline expansion results in a call error(msgStr) the compiler produces an error message containing the given msgStr. + * + * ```scala + * error("My error message") + * ``` + * or + * ```scala + * error(code"My error of this code: ${println("foo")}") + * ``` + */ + inline def error(inline msg: String): Nothing = ??? + + /** Returns the string representations for code passed in the interpolated values + * ```scala + * inline def logged(p1: => Any) = { + * val c = code"code: $p1" + * val res = p1 + * (c, p1) + * } + * logged(indentity("foo")) + * ``` + * is equivalent to: + * ```scala + * ("code: indentity("foo")", indentity("foo")) + * ``` + */ + inline def (self: => StringContext) code (args: => Any*): String = ??? inline def constValueOpt[T]: Option[T] = ??? diff --git a/tests/neg/i6622.scala b/tests/neg/i6622.scala new file mode 100644 index 000000000000..d2002ed1370f --- /dev/null +++ b/tests/neg/i6622.scala @@ -0,0 +1,9 @@ +import scala.compiletime._ + +object Test { + + def main(args: Array[String]): Unit = { + println(StringContext("abc ", "", "").code(println(34))) // error + } + +} diff --git a/tests/neg/i6622a.scala b/tests/neg/i6622a.scala new file mode 100644 index 000000000000..1b716444eb63 --- /dev/null +++ b/tests/neg/i6622a.scala @@ -0,0 +1,11 @@ +import scala.compiletime._ + +object Test { + + def nonConstant: String = "" + + def main(args: Array[String]): Unit = { + println(StringContext("abc ", nonConstant).code(println(34))) // error + } + +} diff --git a/tests/neg/i6622b.scala b/tests/neg/i6622b.scala new file mode 100644 index 000000000000..48c8a24112cc --- /dev/null +++ b/tests/neg/i6622b.scala @@ -0,0 +1,9 @@ +import scala.compiletime._ + +object Test { + + def main(args: Array[String]): Unit = { + println(StringContext("abc ").code(println(34), 34)) // error + } + +} diff --git a/tests/neg/i6622c.scala b/tests/neg/i6622c.scala new file mode 100644 index 000000000000..f9483d7fabdc --- /dev/null +++ b/tests/neg/i6622c.scala @@ -0,0 +1,9 @@ +import scala.compiletime._ + +object Test { + + def main(args: Array[String]): Unit = { + println(StringContext(Seq.empty[String]:_*).code(println(34))) // error + } + +} diff --git a/tests/neg/i6622d.scala b/tests/neg/i6622d.scala new file mode 100644 index 000000000000..1592086f67a6 --- /dev/null +++ b/tests/neg/i6622d.scala @@ -0,0 +1,9 @@ +import scala.compiletime._ + +object Test { + + def main(args: Array[String]): Unit = { + println(StringContext("abc").code(Seq.empty[Any]:_*)) // error + } + +} diff --git a/tests/neg/i6622e.scala b/tests/neg/i6622e.scala new file mode 100644 index 000000000000..626c15bf4ef8 --- /dev/null +++ b/tests/neg/i6622e.scala @@ -0,0 +1,9 @@ +import scala.compiletime._ + +object Test { + + def main(args: Array[String]): Unit = { + println(StringContext(Seq.empty[String]:_*).code(Seq.empty[Any]:_*)) // error + } + +} diff --git a/tests/neg/i6622f.check b/tests/neg/i6622f.check new file mode 100644 index 000000000000..d30826f0475d --- /dev/null +++ b/tests/neg/i6622f.check @@ -0,0 +1,4 @@ +-- Error: tests/neg/i6622f.scala:6:8 ----------------------------------------------------------------------------------- +6 | fail(println("foo")) // error + | ^^^^^^^^^^^^^^^^^^^^ + | failed: println("foo") ... diff --git a/tests/neg/i6622f.scala b/tests/neg/i6622f.scala new file mode 100644 index 000000000000..e90a44172d29 --- /dev/null +++ b/tests/neg/i6622f.scala @@ -0,0 +1,11 @@ +import scala.compiletime._ + +object Test { + + def main(args: Array[String]): Unit = { + fail(println("foo")) // error + } + + inline def fail(p1: => Any) = error(code"failed: $p1 ...") + +} diff --git a/tests/run/i6622.scala b/tests/run/i6622.scala new file mode 100644 index 000000000000..e541a8aa5c58 --- /dev/null +++ b/tests/run/i6622.scala @@ -0,0 +1,15 @@ +import scala.compiletime._ + +object Test { + + def main(args: Array[String]): Unit = { + assert(code"abc ${println(34)} ..." == "abc println(34) ...") + assert(code"abc ${println(34)}" == "abc println(34)") + assert(code"${println(34)} ..." == "println(34) ...") + assert(code"${println(34)}" == "println(34)") + assert(code"..." == "...") + assert(testConstant(code"") == "") + } + + inline def testConstant(inline msg: String): String = msg +}