Skip to content

Proposal: Decouple Dotty macros from inlining #5122

Closed
@allanrenucci

Description

@allanrenucci

Currently macros in Dotty rely on inlining. A call to a macro is first inlined at call site, then the macro is evaluated. However, inlining must preserve the semantic of the program and perform unpredictable tree transformations.

Macros authors now need to understand what happens during inlining, massage the inline definition to get the trees they expect and reverse some transformation performed by the inliner (e.g. deal with Inline trees).

Let's look through an example that illustrate my point. We would like to implement a macro that can inspect its receiver and its arguments.

import scala.quoted._

class Foo {
  rewrite def myMacro(x: Int): Int = ~Foo.macroImpl('(this), '(x))
}

object Foo {
  def macroImpl(foo: Expr[Foo], x: Expr[Int]): Expr[Int] = // do something
}

And a simple use case:

def foo() = new Foo
def bar() = 1
foo().myMacro(bar())

Here is what we get after inlining:

val x: Int = this.bar()
val Foo_this: Foo = this.foo()
~Foo.macroImpl(Expr(Foo_this), Expr(x))

The inliner lifted out the receiver and the argument of the call to the macro and only give us a reference to them. To workaround this issue, one needs to massage the macro definition a lot in order to get the expected trees:

import scala.quoted._

class Foo

class FooOps(foo: => Foo) {
  rewrite def myMacro(x: => Int) = ~Foo.macroImpl('(foo), '(x))
}

object Foo {
  // now implicit conversion must be in scope to call the macro
  implicit rewrite def FooOps(foo: => Foo): FooOps = new FooOps(foo)

  def macroImpl(foo: Expr[Foo], x: Expr[Int]): Expr[Int] = x
}

Note: This can possibly improve with extension methods

And here is what we get after inlining:

// This can possibly be dead code eliminated if `FooOps` and `foo` are proven pure.
// More massaging can help: FooOps extends AnyVal
val FooOps_this: FooOps =
  new FooOps(this.foo())

Foo.macroImpl(
  Expr(this.foo()),
  Expr(this.bar())
)

Our macro can now inspect inspect the trees of its receiver and its arguments. This is a lot of ceremony for something that is straightforward in scalac and I believe a common use case of macros.

I propose to decouple inlining from macros and re-use the semantic we have in scalac:

For a call receiver.myMacro(args), if myMacro is a macro, the compiler will expand that application by invoking the corresponding macro implementation method, with the abstract-syntax trees of the argument expressions receiver and args as arguments.

One could imagine the syntax being something like (this is not a proposal about the syntax):

class Foo {
  macro def myMacro(x: Int): Int = ~Foo.macroImpl('(this), '(x))
}
object Foo {
  def macroImpl(foo: Expr[Foo], x: Expr[Int]): Expr[Int] = // do something
}

@xeno-by, @liufengyun, @olafurpg, @nicolasstucki I would like to here your opinion and know if there are any concerns or drawbacks about going back to the Scala 2 semantic. I also discussed this with @sjrd and @OlivierBlanvillain.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions