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
scrutinizewhich doesn't receive an expression argument, and returns a canned expression'{2}(and has return typeExpr[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