From 13bc4983d6863123eb7ec96cb70cbc724743a533 Mon Sep 17 00:00:00 2001 From: Nicolas Stucki Date: Tue, 8 Oct 2019 17:39:12 +0200 Subject: [PATCH 1/2] Add `let` abstractions with customizable and fresh names --- .../dotc/tastyreflect/QuoteContextState.scala | 22 +++++++++ .../ReflectionCompilerInterface.scala | 3 ++ .../src/dotty/tools/dotc/typer/Inliner.scala | 2 +- docs/docs/reference/metaprogramming/macros.md | 46 +++++++++++++++++++ .../scala/quoted/util/package.scala | 17 +++++++ library/src/scala/quoted/QuoteContext.scala | 6 +++ .../tasty/reflect/CompilerInterface.scala | 2 + .../scala/quoted/staging/QuoteDriver.scala | 4 +- tests/run-macros/quoted-let-name-3.check | 23 ++++++++++ .../quoted-let-name-3/Macro_1.scala | 14 ++++++ .../run-macros/quoted-let-name-3/Test_2.scala | 8 ++++ tests/run-staging/quoted-let-name-1.check | 13 ++++++ tests/run-staging/quoted-let-name-1.scala | 20 ++++++++ tests/run-staging/quoted-let-name-2.check | 13 ++++++ tests/run-staging/quoted-let-name-2.scala | 20 ++++++++ 15 files changed, 210 insertions(+), 3 deletions(-) create mode 100644 compiler/src/dotty/tools/dotc/tastyreflect/QuoteContextState.scala create mode 100644 library/src-bootstrapped/scala/quoted/util/package.scala create mode 100644 tests/run-macros/quoted-let-name-3.check create mode 100644 tests/run-macros/quoted-let-name-3/Macro_1.scala create mode 100644 tests/run-macros/quoted-let-name-3/Test_2.scala create mode 100644 tests/run-staging/quoted-let-name-1.check create mode 100644 tests/run-staging/quoted-let-name-1.scala create mode 100644 tests/run-staging/quoted-let-name-2.check create mode 100644 tests/run-staging/quoted-let-name-2.scala diff --git a/compiler/src/dotty/tools/dotc/tastyreflect/QuoteContextState.scala b/compiler/src/dotty/tools/dotc/tastyreflect/QuoteContextState.scala new file mode 100644 index 000000000000..61cc105dbdf8 --- /dev/null +++ b/compiler/src/dotty/tools/dotc/tastyreflect/QuoteContextState.scala @@ -0,0 +1,22 @@ +package dotty.tools.dotc.tastyreflect + +import dotty.tools.dotc.ast.tpd +import dotty.tools.dotc.core._ +import dotty.tools.dotc.core.Contexts._ +import dotty.tools.dotc.util.Property + +object QuoteContextState { + + val NextIndex = new Property.Key[scala.runtime.IntRef] + + def init(ctx: Context): Context = + ctx.fresh.setProperty(NextIndex, new scala.runtime.IntRef(0)) + + def nextIndex()(given ctx: Context): Int = { + val ref = ctx.property(NextIndex).get + val ret = ref.elem + ref.elem = ret + 1 + ret + } +} + diff --git a/compiler/src/dotty/tools/dotc/tastyreflect/ReflectionCompilerInterface.scala b/compiler/src/dotty/tools/dotc/tastyreflect/ReflectionCompilerInterface.scala index 3c3d410b50d0..d163d60c19fa 100644 --- a/compiler/src/dotty/tools/dotc/tastyreflect/ReflectionCompilerInterface.scala +++ b/compiler/src/dotty/tools/dotc/tastyreflect/ReflectionCompilerInterface.scala @@ -12,6 +12,7 @@ import dotty.tools.dotc.core.Symbols._ import dotty.tools.dotc.core.Decorators._ import dotty.tools.dotc.core.Types.SingletonType import dotty.tools.dotc.tastyreflect.FromSymbol.{definitionFromSym, packageDefFromSym} +import dotty.tools.dotc.tastyreflect.QuoteContextState import dotty.tools.dotc.typer.Implicits.{AmbiguousImplicits, DivergingImplicit, NoMatchingImplicits, SearchFailure, SearchFailureType} import dotty.tools.dotc.util.{SourceFile, SourcePosition, Spans} @@ -28,6 +29,8 @@ class ReflectionCompilerInterface(val rootContext: core.Contexts.Context) extend def rootPosition: util.SourcePosition = tastyreflect.MacroExpansion.position.getOrElse(SourcePosition(rootContext.source, Spans.NoSpan)) + def nextIndex(): Int = QuoteContextState.nextIndex() + // // QUOTE UNPICKLING // diff --git a/compiler/src/dotty/tools/dotc/typer/Inliner.scala b/compiler/src/dotty/tools/dotc/typer/Inliner.scala index 64af560f8847..2f9a80ada12d 100644 --- a/compiler/src/dotty/tools/dotc/typer/Inliner.scala +++ b/compiler/src/dotty/tools/dotc/typer/Inliner.scala @@ -1236,7 +1236,7 @@ class Inliner(call: tpd.Tree, rhsToInline: tpd.Tree)(implicit ctx: Context) { private def expandMacro(body: Tree, span: Span)(implicit ctx: Context) = { assert(level == 0) val inlinedFrom = enclosingInlineds.last - val ctx1 = tastyreflect.MacroExpansion.context(inlinedFrom) + val ctx1 = tastyreflect.QuoteContextState.init(tastyreflect.MacroExpansion.context(inlinedFrom)) val dependencies = macroDependencies(body) diff --git a/docs/docs/reference/metaprogramming/macros.md b/docs/docs/reference/metaprogramming/macros.md index 0ad19aa737fa..eb47c804e0bf 100644 --- a/docs/docs/reference/metaprogramming/macros.md +++ b/docs/docs/reference/metaprogramming/macros.md @@ -564,6 +564,52 @@ while (i < arr.length) { } sum ``` +### Showing meaningful definition names in quotes + +In the `powerCode` example above there is a `'{ val y = $x * $x; ... }` which when printed +may show several different `val y = ...`. + +For example +```scala +powerCode(16, '{7}).show +``` +will show +```scala +val y: scala.Double = 7 +val y: scala.Double = y.*(y) +val y: scala.Double = y.*(y) +val y: scala.Double = y.*(y) +val y: scala.Double = y.*(y) +y +``` +Even though there is no hygiene issue it may be hard to undestand the code. To overcome this inconvenience +each `y` can be assigned a meaningful name using the `scala.quoted.util.let`. +For example `let("y" + i)('{ $x * $x })(yi => ...)` will assign to each `y` a name +`y{i}` where `{i}` is a known String, if `i == 3` then it would be named `y3`. + +The `powerCode` can be defined as follows using `showName` +```scala +def powerCodeD(n: Long, x: Expr[Double]))(given QuoteContext): Expr[Double] = + let("y1")(x)(y1 => powerCodeD(n, 2, y1)) + +def powerCodeD(n: Long, i: Int, x: Expr[Double])(given QuoteContext): Expr[Double] = + if (n == 0) '{1.0} + else if (n % 2 == 0) let("y" + i)('{ $x * $x })(ai => powerCodeD(n / 2, idx * 2, yi) } + else '{ $x * ${powerCodeD(n - 1, idx, x)} } +``` +then +```scala +powerCodeD(16, '{7}).show +``` +will show +```scala +val y1: scala.Double = 7 +val y2: scala.Double = y1.*(y1) +val y4: scala.Double = y2.*(y2) +val y8: scala.Double = y4.*(y4) +val y16: scala.Double = y8.*(y8) +a16 +``` ### Find implicits within a macro diff --git a/library/src-bootstrapped/scala/quoted/util/package.scala b/library/src-bootstrapped/scala/quoted/util/package.scala new file mode 100644 index 000000000000..0dce8dd42204 --- /dev/null +++ b/library/src-bootstrapped/scala/quoted/util/package.scala @@ -0,0 +1,17 @@ +package scala.quoted + +import scala.internal.quoted.showName + +package object util { + + /** Genrate the code `'{ val `` = $expr; ${ body(') } }` with the given name */ + def let[T: Type, U: Type](name: String)(expr: Expr[T])(body: Expr[T] => Expr[U])(given QuoteContext): Expr[U] = '{ + @showName(${Expr(name)}) val x = $expr + ${ body('x) } + } + + /** Genrate the code `'{ val x$ = $expr; ${ body('x$) } }` for a fresh i */ + def let[T: Type, U: Type](expr: Expr[T])(body: Expr[T] => Expr[U])(given qctx: QuoteContext): Expr[U] = + let("x$" + qctx.nextIndex())(expr)(body) + +} diff --git a/library/src/scala/quoted/QuoteContext.scala b/library/src/scala/quoted/QuoteContext.scala index 6831663e4a0f..a038e456c107 100644 --- a/library/src/scala/quoted/QuoteContext.scala +++ b/library/src/scala/quoted/QuoteContext.scala @@ -46,6 +46,12 @@ class QuoteContext(val tasty: scala.tasty.Reflection) { tasty.warning(msg, expr.unseal.pos)(given rootContext) } + /** Get a fresh index within the scope of this quote context. + * Each `scala.quoted.staging.run` and macro expansion starts with the next index at 0. + */ + def nextIndex(): Int = + tasty.internal.nextIndex() + } object QuoteContext { diff --git a/library/src/scala/tasty/reflect/CompilerInterface.scala b/library/src/scala/tasty/reflect/CompilerInterface.scala index 1fa05958fbbb..a58b60efd9f1 100644 --- a/library/src/scala/tasty/reflect/CompilerInterface.scala +++ b/library/src/scala/tasty/reflect/CompilerInterface.scala @@ -117,6 +117,8 @@ trait CompilerInterface { def settings: Settings + def nextIndex(): Int + // // QUOTE UNPICKLING // diff --git a/staging/src/scala/quoted/staging/QuoteDriver.scala b/staging/src/scala/quoted/staging/QuoteDriver.scala index 68441e61cf84..00558fabada9 100644 --- a/staging/src/scala/quoted/staging/QuoteDriver.scala +++ b/staging/src/scala/quoted/staging/QuoteDriver.scala @@ -4,7 +4,7 @@ package staging import dotty.tools.dotc.ast.tpd import dotty.tools.dotc.Driver import dotty.tools.dotc.core.Contexts.{Context, ContextBase, FreshContext} -import dotty.tools.dotc.tastyreflect.ReflectionImpl +import dotty.tools.dotc.tastyreflect.{ReflectionImpl, QuoteContextState} import dotty.tools.io.{AbstractFile, Directory, PlainDirectory, VirtualDirectory} import dotty.tools.repl.AbstractFileClassLoader import dotty.tools.dotc.reporting._ @@ -32,7 +32,7 @@ private class QuoteDriver(appClassloader: ClassLoader) extends Driver { } val (_, ctx0: Context) = setup(settings.compilerArgs.toArray :+ "dummy.scala", initCtx.fresh) - val ctx = setToolboxSettings(ctx0.fresh.setSetting(ctx0.settings.outputDir, outDir), settings) + val ctx = setToolboxSettings(QuoteContextState.init(ctx0).fresh.setSetting(ctx0.settings.outputDir, outDir), settings) new QuoteCompiler().newRun(ctx).compileExpr(exprBuilder) match { case Right(value) => diff --git a/tests/run-macros/quoted-let-name-3.check b/tests/run-macros/quoted-let-name-3.check new file mode 100644 index 000000000000..54af613981d9 --- /dev/null +++ b/tests/run-macros/quoted-let-name-3.check @@ -0,0 +1,23 @@ +Test.x.*({ + val x$0: scala.Double = Test.x.*(Test.x) + val x$1: scala.Double = x$0.*(x$0) + x$1.*({ + val x$2: scala.Double = x$1.*(x$1) + x$2.*({ + val x$3: scala.Double = x$2.*(x$2) + val x$4: scala.Double = x$3.*(x$3) + val x$5: scala.Double = x$4.*(x$4) + x$5 + }) + }) +}) +Test.x.*({ + val x$0: scala.Double = Test.x.*(Test.x) + x$0.*({ + val x$1: scala.Double = x$0.*(x$0) + val x$2: scala.Double = x$1.*(x$1) + val x$3: scala.Double = x$2.*(x$2) + val x$4: scala.Double = x$3.*(x$3) + x$4 + }) +}) diff --git a/tests/run-macros/quoted-let-name-3/Macro_1.scala b/tests/run-macros/quoted-let-name-3/Macro_1.scala new file mode 100644 index 000000000000..67f37b142b51 --- /dev/null +++ b/tests/run-macros/quoted-let-name-3/Macro_1.scala @@ -0,0 +1,14 @@ +import scala.quoted._ +import scala.quoted.util.let + +inline def power(x: Double, inline n: Long): String = + ${ powerCodeShow('x, n) } + +private def powerCodeShow(x: Expr[Double], n: Long)(given QuoteContext): Expr[String] = + Expr(powerCode(n, 2, x).show) + +private def powerCode(n: Long, idx: Int, x: Expr[Double])(given QuoteContext): Expr[Double] = + if (n == 0) '{1.0} + else if (n == 1) x + else if (n % 2 == 0) let('{ $x * $x })(y => powerCode(n / 2, idx * 2, y)) + else '{ $x * ${powerCode(n - 1, idx, x)} } diff --git a/tests/run-macros/quoted-let-name-3/Test_2.scala b/tests/run-macros/quoted-let-name-3/Test_2.scala new file mode 100644 index 000000000000..17ef733809d9 --- /dev/null +++ b/tests/run-macros/quoted-let-name-3/Test_2.scala @@ -0,0 +1,8 @@ + + + +object Test extends App { + val x = 4.5 + println(power(x, 77)) + println(power(x, 35)) +} diff --git a/tests/run-staging/quoted-let-name-1.check b/tests/run-staging/quoted-let-name-1.check new file mode 100644 index 000000000000..6a28d9267ace --- /dev/null +++ b/tests/run-staging/quoted-let-name-1.check @@ -0,0 +1,13 @@ +((x: scala.Double) => x.*({ + val x$0: scala.Double = x.*(x) + val x$1: scala.Double = x$0.*(x$0) + x$1.*({ + val x$2: scala.Double = x$1.*(x$1) + x$2.*({ + val x$3: scala.Double = x$2.*(x$2) + val x$4: scala.Double = x$3.*(x$3) + val x$5: scala.Double = x$4.*(x$4) + x$5 + }) + }) +})) diff --git a/tests/run-staging/quoted-let-name-1.scala b/tests/run-staging/quoted-let-name-1.scala new file mode 100644 index 000000000000..9f3fcf9af72b --- /dev/null +++ b/tests/run-staging/quoted-let-name-1.scala @@ -0,0 +1,20 @@ +import scala.quoted._ +import scala.quoted.util.let +import scala.quoted.staging._ + +object Test { + given Toolbox = Toolbox.make(getClass.getClassLoader) + def main(args: Array[String]): Unit = withQuoteContext { + println(powerCode(77).show) + } + + def powerCode(n: Long)(given QuoteContext): Expr[Double => Double] = + '{ x => ${powerCode(n, 2, 'x)} } + + def powerCode(n: Long, idx: Int, x: Expr[Double])(given QuoteContext): Expr[Double] = + if (n == 0) '{1.0} + else if (n == 1) x + else if (n % 2 == 0) let('{ $x * $x })(y => powerCode(n / 2, idx * 2, y)) + else '{ $x * ${powerCode(n - 1, idx, x)} } + +} diff --git a/tests/run-staging/quoted-let-name-2.check b/tests/run-staging/quoted-let-name-2.check new file mode 100644 index 000000000000..ae3f5e6928af --- /dev/null +++ b/tests/run-staging/quoted-let-name-2.check @@ -0,0 +1,13 @@ +((x1: scala.Double) => x1.*({ + val x2: scala.Double = x1.*(x1) + val x4: scala.Double = x2.*(x2) + x4.*({ + val x8: scala.Double = x4.*(x4) + x8.*({ + val x16: scala.Double = x8.*(x8) + val x32: scala.Double = x16.*(x16) + val x64: scala.Double = x32.*(x32) + x64 + }) + }) +})) diff --git a/tests/run-staging/quoted-let-name-2.scala b/tests/run-staging/quoted-let-name-2.scala new file mode 100644 index 000000000000..4c7b95d5ae0f --- /dev/null +++ b/tests/run-staging/quoted-let-name-2.scala @@ -0,0 +1,20 @@ +import scala.quoted._ +import scala.quoted.util.let +import scala.quoted.staging._ + +object Test { + given Toolbox = Toolbox.make(getClass.getClassLoader) + def main(args: Array[String]): Unit = withQuoteContext { + println(powerCode(77).show) + } + + def powerCode(n: Long)(given QuoteContext): Expr[Double => Double] = + '{ x1 => ${powerCode(n, 2, 'x1)} } + + def powerCode(n: Long, idx: Int, x: Expr[Double])(given QuoteContext): Expr[Double] = + if (n == 0) '{1.0} + else if (n == 1) x + else if (n % 2 == 0) let(s"x$idx")('{ $x * $x })(y => powerCode(n / 2, idx * 2, y)) + else '{ $x * ${powerCode(n - 1, idx, x)} } + +} From e0c312b419b7f4a2838e6ea3cca80faf508ca954 Mon Sep 17 00:00:00 2001 From: Liu Fengyun Date: Mon, 14 Oct 2019 09:56:00 +0200 Subject: [PATCH 2/2] Fix CI --- community-build/community-projects/scalatest | 2 +- tests/pos-macros/i6535/Macro_1.scala | 1 - tests/run-macros/i6171/Macro_1.scala | 1 - tests/run-macros/reflect-dsl/assert_1.scala | 1 - tests/run-macros/reflect-pos-fun/assert_1.scala | 1 - tests/run-macros/reflect-select-constructor/assert_1.scala | 1 - .../run-macros/reflect-select-symbol-constructor/assert_1.scala | 1 - tests/run-macros/reflect-select-value-class/assert_1.scala | 1 - 8 files changed, 1 insertion(+), 8 deletions(-) diff --git a/community-build/community-projects/scalatest b/community-build/community-projects/scalatest index 08f8a5c0f8f3..cb1191677748 160000 --- a/community-build/community-projects/scalatest +++ b/community-build/community-projects/scalatest @@ -1 +1 @@ -Subproject commit 08f8a5c0f8f3a3e8d25e0320e171b386e0c44bb1 +Subproject commit cb1191677748cde23767e68c7f1a053bfa6d1cc1 diff --git a/tests/pos-macros/i6535/Macro_1.scala b/tests/pos-macros/i6535/Macro_1.scala index 619197c61c9b..25a157ec994b 100644 --- a/tests/pos-macros/i6535/Macro_1.scala +++ b/tests/pos-macros/i6535/Macro_1.scala @@ -6,7 +6,6 @@ object scalatest { def assertImpl(cond: Expr[Boolean])(given qctx: QuoteContext): Expr[Unit] = { import qctx.tasty.{_, given} - import util._ cond.unseal.underlyingArgument match { case t @ Apply(Select(lhs, op), rhs :: Nil) => diff --git a/tests/run-macros/i6171/Macro_1.scala b/tests/run-macros/i6171/Macro_1.scala index 729acd0d5d59..6ff2197d6c6c 100644 --- a/tests/run-macros/i6171/Macro_1.scala +++ b/tests/run-macros/i6171/Macro_1.scala @@ -6,7 +6,6 @@ object scalatest { def assertImpl(cond: Expr[Boolean], clue: Expr[Any])(given qctx: QuoteContext): Expr[Unit] = { import qctx.tasty.{_, given} - import util._ def isImplicitMethodType(tp: Type): Boolean = Type.IsMethodType.unapply(tp).flatMap(tp => if tp.isImplicit then Some(true) else None).nonEmpty diff --git a/tests/run-macros/reflect-dsl/assert_1.scala b/tests/run-macros/reflect-dsl/assert_1.scala index d063cf16d1e8..f16d854652c0 100644 --- a/tests/run-macros/reflect-dsl/assert_1.scala +++ b/tests/run-macros/reflect-dsl/assert_1.scala @@ -6,7 +6,6 @@ object scalatest { def assertImpl(cond: Expr[Boolean], clue: Expr[Any])(given qctx: QuoteContext): Expr[Unit] = { import qctx.tasty.{_, given} - import util._ def isImplicitMethodType(tp: Type): Boolean = Type.IsMethodType.unapply(tp).flatMap(tp => if tp.isImplicit then Some(true) else None).nonEmpty diff --git a/tests/run-macros/reflect-pos-fun/assert_1.scala b/tests/run-macros/reflect-pos-fun/assert_1.scala index c6902daafd46..efa5044f0084 100644 --- a/tests/run-macros/reflect-pos-fun/assert_1.scala +++ b/tests/run-macros/reflect-pos-fun/assert_1.scala @@ -6,7 +6,6 @@ object scalatest { def assertImpl(cond: Expr[Boolean])(given qctx: QuoteContext): Expr[Unit] = { import qctx.tasty.{_, given} - import util._ cond.unseal.underlyingArgument match { case t @ Apply(TypeApply(Select(lhs, op), targs), rhs) => diff --git a/tests/run-macros/reflect-select-constructor/assert_1.scala b/tests/run-macros/reflect-select-constructor/assert_1.scala index 0177198e6aef..14e1eda64f62 100644 --- a/tests/run-macros/reflect-select-constructor/assert_1.scala +++ b/tests/run-macros/reflect-select-constructor/assert_1.scala @@ -6,7 +6,6 @@ object scalatest { def assertImpl(cond: Expr[Boolean], clue: Expr[Any])(given qctx: QuoteContext): Expr[Unit] = { import qctx.tasty.{_, given} - import util._ def isImplicitMethodType(tp: Type): Boolean = Type.IsMethodType.unapply(tp).flatMap(tp => if tp.isImplicit then Some(true) else None).nonEmpty diff --git a/tests/run-macros/reflect-select-symbol-constructor/assert_1.scala b/tests/run-macros/reflect-select-symbol-constructor/assert_1.scala index a6de05000b75..f867d8873a33 100644 --- a/tests/run-macros/reflect-select-symbol-constructor/assert_1.scala +++ b/tests/run-macros/reflect-select-symbol-constructor/assert_1.scala @@ -6,7 +6,6 @@ object scalatest { def assertImpl(cond: Expr[Boolean], clue: Expr[Any])(given qctx: QuoteContext): Expr[Unit] = { import qctx.tasty.{_, given} - import util._ def isImplicitMethodType(tp: Type): Boolean = Type.IsMethodType.unapply(tp).flatMap(tp => if tp.isImplicit then Some(true) else None).nonEmpty diff --git a/tests/run-macros/reflect-select-value-class/assert_1.scala b/tests/run-macros/reflect-select-value-class/assert_1.scala index 0177198e6aef..14e1eda64f62 100644 --- a/tests/run-macros/reflect-select-value-class/assert_1.scala +++ b/tests/run-macros/reflect-select-value-class/assert_1.scala @@ -6,7 +6,6 @@ object scalatest { def assertImpl(cond: Expr[Boolean], clue: Expr[Any])(given qctx: QuoteContext): Expr[Unit] = { import qctx.tasty.{_, given} - import util._ def isImplicitMethodType(tp: Type): Boolean = Type.IsMethodType.unapply(tp).flatMap(tp => if tp.isImplicit then Some(true) else None).nonEmpty