Skip to content

Commit 51de940

Browse files
committed
Add syntax for higher-order quote pattern holes
* Make typer explicitly type higher-order quasiquotes * Add higher-order quasi-pattern concept to the Matcher (avoid ambiguities with splice applied to arguments) * Introduce unambiguous syntax for higher-order quasi-patterns * Add documentation * Warn if old syntax is detected
1 parent 68deb63 commit 51de940

File tree

18 files changed

+209
-20
lines changed

18 files changed

+209
-20
lines changed

compiler/src/dotty/tools/dotc/core/Definitions.scala

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -695,6 +695,8 @@ class Definitions {
695695
@tu lazy val InternalQuoted_exprNestedSplice : Symbol = InternalQuotedModule.requiredMethod("exprNestedSplice")
696696
@tu lazy val InternalQuoted_typeQuote : Symbol = InternalQuotedModule.requiredMethod("typeQuote")
697697
@tu lazy val InternalQuoted_patternHole: Symbol = InternalQuotedModule.requiredMethod("patternHole")
698+
@tu lazy val InternalQuoted_patternHigherOrderHole: Symbol = InternalQuotedModule.requiredMethod("patternHigherOrderHole")
699+
@tu lazy val InternalQuoted_higherOrderHole: Symbol = InternalQuotedModule.requiredMethod("higherOrderHole")
698700
@tu lazy val InternalQuoted_patternTypeAnnot: ClassSymbol = InternalQuotedModule.requiredClass("patternType")
699701
@tu lazy val InternalQuoted_QuoteTypeTagAnnot: ClassSymbol = InternalQuotedModule.requiredClass("quoteTypeTag")
700702
@tu lazy val InternalQuoted_fromAboveAnnot: ClassSymbol = InternalQuotedModule.requiredClass("fromAbove")

compiler/src/dotty/tools/dotc/tastyreflect/ReflectionCompilerInterface.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1983,6 +1983,7 @@ class ReflectionCompilerInterface(val rootContext: core.Contexts.Context) extend
19831983
def Definitions_isTupleClass(sym: Symbol): Boolean = defn.isTupleClass(sym)
19841984

19851985
def Definitions_InternalQuoted_patternHole: Symbol = defn.InternalQuoted_patternHole
1986+
def Definitions_InternalQuoted_higherOrderHole: Symbol = defn.InternalQuoted_higherOrderHole
19861987
def Definitions_InternalQuoted_patternTypeAnnot: Symbol = defn.InternalQuoted_patternTypeAnnot
19871988
def Definitions_InternalQuoted_fromAboveAnnot: Symbol = defn.InternalQuoted_fromAboveAnnot
19881989

compiler/src/dotty/tools/dotc/typer/QuotesAndSplices.scala

Lines changed: 51 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -75,13 +75,41 @@ trait QuotesAndSplices {
7575
}
7676
if (ctx.mode.is(Mode.QuotedPattern))
7777
if (isFullyDefined(pt, ForceDegree.flipBottom)) {
78-
def spliceOwner(ctx: Context): Symbol =
79-
if (ctx.mode.is(Mode.QuotedPattern)) spliceOwner(ctx.outer) else ctx.owner
80-
val pat = typedPattern(tree.expr, defn.QuotedExprClass.typeRef.appliedTo(pt))(
81-
using spliceContext.retractMode(Mode.QuotedPattern).withOwner(spliceOwner(ctx)))
82-
val baseType = pat.tpe.baseType(defn.QuotedExprClass)
83-
val argType = if baseType != NoType then baseType.argTypesHi.head else defn.NothingType
84-
ref(defn.InternalQuoted_exprSplice).appliedToType(argType).appliedTo(pat)
78+
object HigherOrderQuasipattern {
79+
/** Matches a splice of the form `${<pat> $$ <args*>}` */
80+
def unapply(tree: untpd.Tree): Option[(untpd.Tree, List[untpd.Tree])] =
81+
tree match
82+
case untpd.InfixOp(pat, untpd.Ident(nme.EXPAND_SEPARATOR), args0) =>
83+
val args = args0 match
84+
case untpd.Tuple(args) => args
85+
case arg => List(arg)
86+
Some((pat, args))
87+
case Apply(Select(pat, nme.EXPAND_SEPARATOR), args) => Some((pat, args))
88+
case _ => None
89+
}
90+
tree.expr match
91+
case HigherOrderQuasipattern(pat, args) =>
92+
val typedArgs = args.map {
93+
case arg: untpd.Ident =>
94+
typedExpr(arg)
95+
case arg =>
96+
ctx.error("Exprected an identifier", arg.sourcePos)
97+
EmptyTree
98+
}
99+
if args.isEmpty then
100+
ctx.error("Missing arguments for open pattern", tree.expr.sourcePos)
101+
val argTypes = typedArgs.map(_.tpe.widenTermRefExpr)
102+
val splice1 = untpd.cpy.Splice(tree)(pat).asInstanceOf[untpd.Splice]
103+
val typedPat = typedSplice(splice1, defn.FunctionOf(argTypes, pt))
104+
ref(defn.InternalQuoted_patternHigherOrderHole).appliedToType(pt).appliedTo(typedPat, SeqLiteral(typedArgs, TypeTree(defn.AnyType)))
105+
case _ =>
106+
def spliceOwner(ctx: Context): Symbol =
107+
if (ctx.mode.is(Mode.QuotedPattern)) spliceOwner(ctx.outer) else ctx.owner
108+
val pat = typedPattern(tree.expr, defn.QuotedExprClass.typeRef.appliedTo(pt))(
109+
using spliceContext.retractMode(Mode.QuotedPattern).withOwner(spliceOwner(ctx)))
110+
val baseType = pat.tpe.baseType(defn.QuotedExprClass)
111+
val argType = if baseType != NoType then baseType.argTypesHi.head else defn.NothingType
112+
ref(defn.InternalQuoted_exprSplice).appliedToType(argType).appliedTo(pat)
85113
}
86114
else {
87115
ctx.error(i"Type must be fully defined.\nConsider annotating the splice using a type ascription:\n ($tree: XYZ).", tree.expr.sourcePos)
@@ -119,6 +147,14 @@ trait QuotesAndSplices {
119147
def typedAppliedSplice(tree: untpd.Apply, pt: Type)(using Context): Tree = {
120148
assert(ctx.mode.is(Mode.QuotedPattern))
121149
val untpd.Apply(splice: untpd.Splice, args) = tree
150+
if args.forall(_.isInstanceOf[untpd.Ident]) then
151+
val suggestionArgs = args match
152+
case arg :: Nil => arg.show
153+
case args => args.map(_.show).mkString("(", ", ", ")")
154+
val suggestion = s"$${${splice.expr.show} $$$$ $suggestionArgs}"
155+
ctx.warning(s"""Possibly using open pattern syntax instead application pattern.
156+
|If this is a open pattern replace it with $suggestion.
157+
|""".stripMargin, tree.sourcePos)
122158
if (isFullyDefined(pt, ForceDegree.flipBottom)) then
123159
val typedArgs = args.map(arg => typedExpr(arg))
124160
val argTypes = typedArgs.map(_.tpe.widenTermRefExpr)
@@ -224,6 +260,14 @@ trait QuotesAndSplices {
224260
val exprTpt = AppliedTypeTree(TypeTree(defn.QuotedExprClass.typeRef), tpt1 :: Nil)
225261
val newSplice = ref(defn.InternalQuoted_exprSplice).appliedToType(tpt1.tpe).appliedTo(Typed(pat, exprTpt))
226262
transform(newSplice)
263+
case Apply(TypeApply(fn, targs), Apply(sp, pat :: Nil) :: args :: Nil) if fn.symbol == defn.InternalQuoted_patternHigherOrderHole =>
264+
try ref(defn.InternalQuoted_higherOrderHole.termRef).appliedToTypeTrees(targs).appliedTo(args).withSpan(tree.span)
265+
finally {
266+
val patType = pat.tpe.widen
267+
val patType1 = patType.translateFromRepeated(toArray = false)
268+
val pat1 = if (patType eq patType1) pat else pat.withType(patType1)
269+
patBuf += pat1
270+
}
227271
case Apply(fn, pat :: Nil) if fn.symbol == defn.InternalQuoted_exprSplice =>
228272
try ref(defn.InternalQuoted_patternHole.termRef).appliedToType(tree.tpe).withSpan(tree.span)
229273
finally {

docs/docs/reference/metaprogramming/macros.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -744,5 +744,50 @@ trait Show[-T] {
744744
}
745745
```
746746

747+
#### Open code patterns
748+
749+
Quote pattern matching also provides higher-order patterns to match open terms. If a quoted term contains a definition,
750+
then the rest of the quote can refer to this definition.
751+
```
752+
'{
753+
val x: Int = 4
754+
x * x
755+
}
756+
```
757+
758+
To match such a term we need to match the definition and the rest of the code, but we need to expicilty state that the rest of the code may refer to this definition.
759+
```scala
760+
case '{ val y: Int = $x; ${body $$ y}: Int } =>
761+
```
762+
Here `$x` will match any closed expression while `${body $$ y}` will match expression that is closed under `y`. Then
763+
the subxpression of type `Expr[Int]` is bound to `body` as an `Expr[Int => Int]`. The extra argument represents the references to `y`. Usually this expression is used in compination with `Expr.betaReduce` to replace the extra argument.
764+
765+
```scala
766+
inline def eval(inline e: Int): Int = ${ evalExpr('e) }
767+
768+
private def evalExpr(using QuoteContext)(e: Expr[Int]): Expr[Int] = {
769+
e match {
770+
case '{ val y: Int = $x; ${body $$ y}: Int } =>
771+
// body: Expr[Int => Int] where the argument represents references to y
772+
evalExpr(Expr.betaReduce(body)(evalExpr(x)))
773+
case '{ ($x: Int) * ($y: Int) } =>
774+
(x, y) match
775+
case (Const(a), Const(b)) => Expr(a * b)
776+
case _ => e
777+
case _ => e
778+
}
779+
}
780+
```
781+
782+
```scala
783+
eval { // expands to the code: (16: Int)
784+
val x: Int = 4
785+
x * x
786+
}
787+
```
788+
789+
We can also close over several bindings using `${b $$ (a1, a2, ..., an)}`.
790+
791+
747792
### More details
748793
[More details](./macros-spec.md)

library/src-bootstrapped/scala/internal/quoted/CompileTime.scala

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,19 +24,31 @@ object CompileTime {
2424
@compileTimeOnly("Illegal reference to `scala.internal.quoted.CompileTime.typeQuote`")
2525
def typeQuote[T <: AnyKind]: QuoteContext ?=> Type[T] = ???
2626

27+
// TODO move to internal.Quoted.Matcher
2728
/** A splice in a quoted pattern is desugared by the compiler into a call to this method */
2829
@compileTimeOnly("Illegal reference to `scala.internal.quoted.CompileTime.patternHole`")
2930
def patternHole[T]: T = ???
3031

32+
@compileTimeOnly("Illegal reference to `scala.internal.quoted.CompileTime.patternHigherOrderHole`")
33+
/** A higher order splice in a quoted pattern is desugared by the compiler into a call to this method */
34+
def patternHigherOrderHole[U](pat: Any, args: Any*): U = ???
35+
36+
// TODO move to internal.Quoted.Matcher
37+
@compileTimeOnly("Illegal reference to `scala.internal.quoted.CompileTime.higherOrderHole`")
38+
/** A higher order splice in a quoted pattern is desugared by the compiler into a call to this method */
39+
def higherOrderHole[U](args: Any*): U = ???
40+
3141
// TODO remove
3242
/** A splice of a name in a quoted pattern is desugared by wrapping getting this annotation */
3343
@compileTimeOnly("Illegal reference to `scala.internal.quoted.CompileTime.patternBindHole`")
3444
class patternBindHole extends Annotation
3545

46+
// TODO move to internal.Quoted.Matcher
3647
/** A splice of a name in a quoted pattern is that marks the definition of a type splice */
3748
@compileTimeOnly("Illegal reference to `scala.internal.quoted.CompileTime.patternType`")
3849
class patternType extends Annotation
3950

51+
// TODO move to internal.Quoted.Matcher
4052
/** A type pattern that must be aproximated from above */
4153
@compileTimeOnly("Illegal reference to `scala.internal.quoted.CompileTime.fromAbove`")
4254
class fromAbove extends Annotation

library/src/scala/internal/quoted/Matcher.scala

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,8 @@ private[quoted] object Matcher {
240240

241241
/* Higher order term hole */
242242
// Matches an open term and wraps it into a lambda that provides the free variables
243-
// TODO do not encode with `hole`. Maybe use `higherOrderHole[(T1, ..., Tn) => R]((x1: T1, ..., xn: Tn)): R`
243+
// DEPRECATED: replaced with `higherOrderHole`
244+
// TODO: remove case
244245
case (scrutinee, pattern @ Apply(Select(TypeApply(patternHole, List(Inferred())), "apply"), args0 @ IdentArgs(args)))
245246
if patternHole.symbol == internal.Definitions_InternalQuoted_patternHole =>
246247
def bodyFn(lambdaArgs: List[Tree]): Tree = {
@@ -258,6 +259,29 @@ private[quoted] object Matcher {
258259
val res = Lambda(MethodType(names)(_ => argTypes, _ => resType), bodyFn)
259260
matched(res.seal)
260261

262+
/* Higher order term hole */
263+
// Matches an open term and wraps it into a lambda that provides the free variables
264+
case (scrutinee, pattern @ Apply(TypeApply(Ident("higherOrderHole"), List(Inferred())), Repeated(args, _) :: Nil))
265+
if pattern.symbol == internal.Definitions_InternalQuoted_higherOrderHole =>
266+
267+
def bodyFn(lambdaArgs: List[Tree]): Tree = {
268+
val argsMap = args.map(_.symbol).zip(lambdaArgs.asInstanceOf[List[Term]]).toMap
269+
new TreeMap {
270+
override def transformTerm(tree: Term)(using ctx: Context): Term =
271+
tree match
272+
case tree: Ident => summon[Env].get(tree.symbol).flatMap(argsMap.get).getOrElse(tree)
273+
case tree => super.transformTerm(tree)
274+
}.transformTree(scrutinee)
275+
}
276+
val names = args.map {
277+
case Block(List(DefDef("$anonfun", _, _, _, Some(Apply(Ident(name), _)))), _) => name
278+
case arg => arg.symbol.name
279+
}
280+
val argTypes = args.map(x => x.tpe.widenTermRefExpr)
281+
val resType = pattern.tpe
282+
val res = Lambda(MethodType(names)(_ => argTypes, _ => resType), bodyFn)
283+
matched(res.seal)
284+
261285
//
262286
// Match two equivalent trees
263287
//

library/src/scala/tasty/reflect/CompilerInterface.scala

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1511,13 +1511,16 @@ trait CompilerInterface {
15111511
def Definitions_TupleClass(arity: Int): Symbol
15121512
def Definitions_isTupleClass(sym: Symbol): Boolean
15131513

1514-
/** Symbol of scala.internal.Quoted.patternHole */
1514+
/** Symbol of scala.internal.CompileTime.patternHole */
15151515
def Definitions_InternalQuoted_patternHole: Symbol
15161516

1517-
/** Symbol of scala.internal.Quoted.patternType */
1517+
/** Symbol of scala.internal.CompileTime.higherOrderHole */
1518+
def Definitions_InternalQuoted_higherOrderHole: Symbol
1519+
1520+
/** Symbol of scala.internal.CompileTime.patternType */
15181521
def Definitions_InternalQuoted_patternTypeAnnot: Symbol
15191522

1520-
/** Symbol of scala.internal.Quoted.fromAbove */
1523+
/** Symbol of scala.internal.CompileTime.fromAbove */
15211524
def Definitions_InternalQuoted_fromAboveAnnot: Symbol
15221525

15231526
def Definitions_UnitType: Type
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import scala.quoted._
2+
3+
def f(using QuoteContext)(x: Expr[Any]): Unit = {
4+
x match
5+
case '{ val a = 3; $b(a): Int } => // error: Possibly using old-higher order pattern syntax instead application pattern.
6+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import scala.quoted._
2+
3+
def f(using QuoteContext)(x: Expr[Any]) = x match {
4+
case '{ identity(${y $$ x}) } => // error: access to value x from wrong staging level
5+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import scala.quoted._
2+
3+
def f(using QuoteContext)(x: Expr[Any]) = x match {
4+
case '{ val a: Int = 3; ${y $$ identity(a)} } => // error: Exprected an identifier
5+
case '{ identity(${y $$ () }) } => // error: Missing arguments for open pattern
6+
}

tests/run-macros/quote-matcher-symantics-3/quoted_1.scala

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,17 +45,17 @@ object Macros {
4545
case '{ (if ($cond) $thenp else $elsep): $t } =>
4646
'{ $sym.ifThenElse[$t](${lift(cond)}, ${lift(thenp)}, ${lift(elsep)}) }.asInstanceOf[Expr[R[T]]]
4747

48-
case '{ (x0: Int) => $bodyFn(x0): Any } =>
48+
case '{ (x0: Int) => ${bodyFn $$ x0}: Any } =>
4949
val (i, nEnvVar) = freshEnvVar[Int]()
5050
val body2 = UnsafeExpr.open(bodyFn) { (body1, close) => close(body1)(nEnvVar) }
5151
'{ $sym.lam((x: R[Int]) => ${given Env = envWith(i, 'x)(using env); lift(body2)}).asInstanceOf[R[T]] }
5252

53-
case '{ (x0: Boolean) => $bodyFn(x0): Any } =>
53+
case '{ (x0: Boolean) => ${bodyFn $$ x0}: Any } =>
5454
val (i, nEnvVar) = freshEnvVar[Boolean]()
5555
val body2 = UnsafeExpr.open(bodyFn) { (body1, close) => close(body1)(nEnvVar) }
5656
'{ $sym.lam((x: R[Boolean]) => ${given Env = envWith(i, 'x)(using env); lift(body2)}).asInstanceOf[R[T]] }
5757

58-
case '{ (x0: Int => Int) => $bodyFn(x0): Any } =>
58+
case '{ (x0: Int => Int) => ${bodyFn $$ x0}: Any } =>
5959
val (i, nEnvVar) = freshEnvVar[Int => Int]()
6060
val body2 = UnsafeExpr.open(bodyFn) { (body1, close) => close(body1)(nEnvVar) }
6161
'{ $sym.lam((x: R[Int => Int]) => ${given Env = envWith(i, 'x)(using env); lift(body2)}).asInstanceOf[R[T]] }

tests/run-macros/quote-matching-open/Macro_1.scala

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ object Macro {
66

77
def impl(x: Expr[Any])(using QuoteContext): Expr[Any] = {
88
x match {
9-
case '{ (x: Int) => $body(x): Int } => UnsafeExpr.open(body) { (body, close) => close(body)(Expr(2)) }
10-
case '{ (x1: Int, x2: Int) => $body(x1, x2): Int } => UnsafeExpr.open(body) { (body, close) => close(body)(Expr(2), Expr(3)) }
11-
case '{ (x1: Int, x2: Int, x3: Int) => $body(x1, x2, x3): Int } => UnsafeExpr.open(body) { (body, close) => close(body)(Expr(2), Expr(3), Expr(4)) }
9+
case '{ (x: Int) => ${body $$ x}: Int } => UnsafeExpr.open(body) { (body, close) => close(body)(Expr(2)) }
10+
case '{ (x1: Int, x2: Int) => ${body $$ (x1, x2)}: Int } => UnsafeExpr.open(body) { (body, close) => close(body)(Expr(2), Expr(3)) }
11+
case '{ (x1: Int, x2: Int, x3: Int) => ${body $$ (x1, x2, x3)}: Int } => UnsafeExpr.open(body) { (body, close) => close(body)(Expr(2), Expr(3), Expr(4)) }
1212
}
1313
}
1414

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Matched open
2+
((y: scala.Int) => y.*(y))
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import scala.quoted._
2+
3+
inline def test(inline e: Int): String = ${testExpr('e)}
4+
5+
private def testExpr(e: Expr[Int])(using QuoteContext): Expr[String] = {
6+
e match {
7+
case '{ val y: Int = 4; ${body $$ y}: Int } => Expr("Matched open\n" + body.show)
8+
}
9+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
2+
object Test {
3+
def main(args: Array[String]): Unit = {
4+
println(test { val x: Int = 4; x * x })
5+
}
6+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import scala.quoted._
2+
3+
inline def eval(inline e: Int): Int = ${ evalExpr('e) }
4+
5+
private def evalExpr(using QuoteContext)(e: Expr[Int]): Expr[Int] = {
6+
e match {
7+
case '{ val y: Int = $x; ${body $$ y}: Int } =>
8+
evalExpr(Expr.betaReduce(body)(evalExpr(x)))
9+
case '{ ($x: Int) * ($y: Int) } =>
10+
(x, y) match
11+
case (Const(a), Const(b)) => Expr(a * b)
12+
case _ => e
13+
case _ => e
14+
}
15+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
2+
object Test {
3+
def main(args: Array[String]): Unit = {
4+
println(eval {
5+
val x: Int = 4
6+
x * x
7+
})
8+
}
9+
}

tests/run-macros/quoted-pattern-open-expr/Macro_1.scala

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ inline def test(inline e: Int): String = ${testExpr('e)}
55
private def testExpr(e: Expr[Int])(using QuoteContext): Expr[String] = {
66
e match {
77
case '{ val y: Int = 4; $body } => Expr("Matched closed\n" + body.show)
8-
case '{ val y: Int = 4; $body(y): Int } => Expr("Matched open\n" + body.show)
9-
case '{ val y: Int => Int = x => x + 1; $body(y): Int } => Expr("Matched open\n" + body.show)
10-
case '{ def g(x: Int): Int = $body(g, x); g(5) } => Expr("Matched open\n" + body.show)
8+
case '{ val y: Int = 4; ${body $$ y}: Int } => Expr("Matched open\n" + body.show)
9+
case '{ val y: Int => Int = x => x + 1; ${body $$ y}: Int } => Expr("Matched open\n" + body.show)
10+
case '{ def g(x: Int): Int = ${body $$ (g, x)}; g(5) } => Expr("Matched open\n" + body.show)
1111
}
1212
}

0 commit comments

Comments
 (0)