Skip to content

Add quoted let #7388

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions compiler/src/dotty/tools/dotc/tastyreflect/QuoteContextState.scala
Original file line number Diff line number Diff line change
@@ -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
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe reuse FreshNameCreator from the compiler?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would have a global indexing which would make any debugging harder as it would keep undesired state from one macro expansion to the other.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If a macro contains many usage of let, then it faces the same problem.

FreshNameCreator is better in the sense that it keeps different counters for different names, instead of using one global counter.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But it uses a global counter across all files in the compilation run and each macro expansion site which is what we are trying to avoid.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's possible to call def setFreshNames(freshNames: FreshNameCreator) on Context before macro expansion.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe a could try to create a fresh FreshNameCreator for each macro expansion site.


Original file line number Diff line number Diff line change
Expand Up @@ -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}

Expand All @@ -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
//
Expand Down
2 changes: 1 addition & 1 deletion compiler/src/dotty/tools/dotc/typer/Inliner.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
46 changes: 46 additions & 0 deletions docs/docs/reference/metaprogramming/macros.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
17 changes: 17 additions & 0 deletions library/src-bootstrapped/scala/quoted/util/package.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package scala.quoted

import scala.internal.quoted.showName

package object util {

/** Genrate the code `'{ val `<name>` = $expr; ${ body('<name>) } }` 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$<i> = $expr; ${ body('x$<i>) } }` 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)

}
6 changes: 6 additions & 0 deletions library/src/scala/quoted/QuoteContext.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
2 changes: 2 additions & 0 deletions library/src/scala/tasty/reflect/CompilerInterface.scala
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,8 @@ trait CompilerInterface {

def settings: Settings

def nextIndex(): Int

//
// QUOTE UNPICKLING
//
Expand Down
4 changes: 2 additions & 2 deletions staging/src/scala/quoted/staging/QuoteDriver.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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._
Expand Down Expand Up @@ -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) =>
Expand Down
1 change: 0 additions & 1 deletion tests/pos-macros/i6535/Macro_1.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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) =>
Expand Down
1 change: 0 additions & 1 deletion tests/run-macros/i6171/Macro_1.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
23 changes: 23 additions & 0 deletions tests/run-macros/quoted-let-name-3.check
Original file line number Diff line number Diff line change
@@ -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
})
})
14 changes: 14 additions & 0 deletions tests/run-macros/quoted-let-name-3/Macro_1.scala
Original file line number Diff line number Diff line change
@@ -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)} }
8 changes: 8 additions & 0 deletions tests/run-macros/quoted-let-name-3/Test_2.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@



object Test extends App {
val x = 4.5
println(power(x, 77))
println(power(x, 35))
}
1 change: 0 additions & 1 deletion tests/run-macros/reflect-dsl/assert_1.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 0 additions & 1 deletion tests/run-macros/reflect-pos-fun/assert_1.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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) =>
Expand Down
1 change: 0 additions & 1 deletion tests/run-macros/reflect-select-constructor/assert_1.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 0 additions & 1 deletion tests/run-macros/reflect-select-value-class/assert_1.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 13 additions & 0 deletions tests/run-staging/quoted-let-name-1.check
Original file line number Diff line number Diff line change
@@ -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
})
})
}))
20 changes: 20 additions & 0 deletions tests/run-staging/quoted-let-name-1.scala
Original file line number Diff line number Diff line change
@@ -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)} }

}
13 changes: 13 additions & 0 deletions tests/run-staging/quoted-let-name-2.check
Original file line number Diff line number Diff line change
@@ -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
})
})
}))
20 changes: 20 additions & 0 deletions tests/run-staging/quoted-let-name-2.scala
Original file line number Diff line number Diff line change
@@ -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)} }

}