Why does this Scala 3 macros reference example of HOAS fail with "Type must be fully defined" error?

73 Views Asked by At

I'm new to scala 3 macros, and am trying to learn my way around them. In trying to learn how to use expression pattern matching, I tried to plug in this example from the reference [1]:

'{ ((x: Int) => x + 1).apply(2) } match
  case '{ ((y: Int) => $f(y)).apply($z: Int) } =>
    // f may contain references to `x` (replaced by `$y`)
    // f = (y: Expr[Int]) => '{ $y + 1 }
    f(z) // generates '{ 2 + 1 }

My attempt to exercise this leads to a compile failure. Of course, to run this, one needs an appropriate context, with quotes, etc. I have tried to create a 'minimal' additional scaffolding to test this. I have wrapped this as follows.

In ExprMatchingPlayground.scala:

package macrotest
import scala.quoted.*

object ExprMatchingPlayground {

  inline def foo(whatever: Any): Any = ${scrutinize('whatever)}

  def scrutinize[T](e: Expr[T])(using qctx: Quotes): Expr[T] = {
    import qctx.reflect.*
    // literal copy-paste of reference example
    '{ ((x: Int) => x + 1).apply(2) } match
      case '{ ((y: Int) => $f(y)).apply($z: Int) } =>
        // f may contain references to `x` (replaced by `$y`)
        // f = (y: Expr[Int]) => '{ $y + 1 }
        f(z) // generates '{ 2 + 1 }
    
    e // no-op returns original e
  }
}

and a basic use, at ExprMatchingDemo.scala

package macrotest

object ExprMatchingDemo extends App {
  ExprMatchingPlayground.foo(3 + 4 * 2) // this specific expression doesn't matter
}

My scala version is 3.3.1 In sbt, I've expanded my compile options as

scalacOptions ++= Seq(
  "-Xcheck-macros",
  "-feature",
  "-explain"
)

On compile, I get the following output:

[error] -- Error: /Users/aaron/toolbox/toolbox/src/main/scala/toolbox/macros/tasty/ExprMatchingPlayground.scala:12:28
[error] 12 |      case '{ ((y: Int) => $f(y)).apply($z: Int) } =>
[error]    |                            ^
[error]    |                 Type must be fully defined.
[error]    |                 Consider annotating the splice using a type ascription:
[error]    |                   ($<none>(y): XYZ).
[error] -- [E006] Not Found Error: /Users/aaron/toolbox/toolbox/src/main/scala/toolbox/macros/tasty/ExprMatchingPlayground.scala:15:8
[error] 15 |        f(z) // generates '{ 2 + 1 }
[error]    |        ^
[error]    |        Not found: f
[error]    |----------------------------------------------------------------------------
[error]    | Explanation (enabled by `-explain`)
[error]    |- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
[error]    | The identifier for `f` is not bound, that is,
[error]    | no declaration for this identifier can be found.
[error]    | That can happen, for example, if `f` or its declaration has either been
[error]    | misspelt or if an import is missing.
[error]     ----------------------------------------------------------------------------
[error] Explanation
[error] ===========
[error] The identifier for `f` is not bound, that is,
[error] no declaration for this identifier can be found.
[error] That can happen, for example, if `f` or its declaration has either been
[error] misspelt or if an import is missing.
[error] -- [E006] Not Found Error: /Users/aaron/toolbox/toolbox/src/main/scala/toolbox/macros/tasty/ExprMatchingPlayground.scala:15:10
[error] 15 |        f(z) // generates '{ 2 + 1 }
[error]    |          ^
[error]    |          Not found: z
[error]    |----------------------------------------------------------------------------
[error]    | Explanation (enabled by `-explain`)
[error]    |- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
[error]    | The identifier for `z` is not bound, that is,
[error]    | no declaration for this identifier can be found.
[error]    | That can happen, for example, if `z` or its declaration has either been
[error]    | misspelt or if an import is missing.
[error]     ----------------------------------------------------------------------------
[error] Explanation
[error] ===========
[error] The identifier for `z` is not bound, that is,
[error] no declaration for this identifier can be found.
[error] That can happen, for example, if `z` or its declaration has either been
[error] misspelt or if an import is missing.

Is this reference example no longer good, or do I need to have something else specific in the surrounding wrapper to make it work? Any comments, suggestions, references, context etc which aren't full answers are still greatly appreciated.

what I've tried

In addition to the specific macro definition and attempted use in the code snippets above, I have also tried variations:

  • removing the import of qctx.reflect.* which I think isn't important here?
  • a version of scrutinize which doesn't receive an expression argument, and returns a canned expression '{2} (and has return type Expr[Int])
  • in my sbt, I have tried setting the scalaVersion to "3.2.0', "3.1.0", and "3.0.0", on the off-chance that this functionality worked in the past and was somehow withdrawn without the docs being appropriately updated -- however, though the specific error message changes, and in some cases other sections of my project also break, some version of the "Type must be fully defined" and "Not found: f" in this short example remain.
  • Though I think this should not be necessary (since I'm using a copy-pasted reference example!), I have tried adding annotations at various points. However, even if I explicitly say
      case '{ ((y: Int) => (($f): Int => Int)(y)).apply($z: Int)} =>
        // f may contain references to `x` (replaced by `$y`)
        // f = (y: Expr[Int]) => '{ $y + 1 }
        f(z) // generates '{ 2 + 1 }

... I get an error saying that f does not take parameters. Of course, f isn't really a function -- it's this HOAS form that represents the body. So what should its type annotation be?

what I'm expecting

Given that this is an example copy-pasted from the scala 3 reference site, I expected:

a. the compile step should run without errors and

b. it should generate the code present in the comment in that reference example (and this could be verified by e.g. binding the result and printing with println(s"generated = ${generated.asTerm.show(using Printer.TreeCode)}"))

However, given my unfamiliarity with scala 3 macros, I'm guessing there may be something basic I'm missing in the context surrounding the pattern-matching.

[1] https://docs.scala-lang.org/scala3/reference/metaprogramming/macros.html#hoas-patterns-1

0

There are 0 best solutions below