Skip to content

Commit 052162d

Browse files
committed
Add runtime.quoted.Matcher
This allows to match a quote against another quote while extracting the contents of defined holes
1 parent 55aae90 commit 052162d

File tree

22 files changed

+991
-15
lines changed

22 files changed

+991
-15
lines changed

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

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1637,21 +1637,7 @@ class KernelImpl(val rootContext: core.Contexts.Context, val rootPosition: util.
16371637

16381638
/** Convert `Term` to an `Expr[T]` and check that it conforms to `T` */
16391639
def QuotedExpr_seal[T](self: Term)(tpe: scala.quoted.Type[T])(implicit ctx: Context): scala.quoted.Expr[T] = {
1640-
16411640
val expectedType = QuotedType_unseal(tpe).tpe
1642-
1643-
def etaExpand(term: Term): Term = term.tpe.widen match {
1644-
case mtpe: Types.MethodType if !mtpe.isParamDependent =>
1645-
val closureResType = mtpe.resType match {
1646-
case t: Types.MethodType => t.toFunctionType()
1647-
case t => t
1648-
}
1649-
val closureTpe = Types.MethodType(mtpe.paramNames, mtpe.paramInfos, closureResType)
1650-
val closureMethod = ctx.newSymbol(ctx.owner, nme.ANON_FUN, Synthetic | Method, closureTpe)
1651-
tpd.Closure(closureMethod, tss => etaExpand(new tpd.TreeOps(term).appliedToArgs(tss.head)))
1652-
case _ => term
1653-
}
1654-
16551641
val expanded = etaExpand(self)
16561642
if (expanded.tpe <:< expectedType) {
16571643
new scala.quoted.Exprs.TastyTreeExpr(expanded).asInstanceOf[scala.quoted.Expr[T]]
@@ -1664,6 +1650,22 @@ class KernelImpl(val rootContext: core.Contexts.Context, val rootPosition: util.
16641650
}
16651651
}
16661652

1653+
/** Convert `Term` to an `Expr[_]` */
1654+
def QuotedExpr_seal2(self: Term)(implicit ctx: Context): scala.quoted.Expr[_] =
1655+
new scala.quoted.Exprs.TastyTreeExpr(etaExpand(self))
1656+
1657+
private def etaExpand(term: Term): Term = term.tpe.widen match {
1658+
case mtpe: Types.MethodType if !mtpe.isParamDependent =>
1659+
val closureResType = mtpe.resType match {
1660+
case t: Types.MethodType => t.toFunctionType()
1661+
case t => t
1662+
}
1663+
val closureTpe = Types.MethodType(mtpe.paramNames, mtpe.paramInfos, closureResType)
1664+
val closureMethod = ctx.newSymbol(ctx.owner, nme.ANON_FUN, Synthetic | Method, closureTpe)
1665+
tpd.Closure(closureMethod, tss => etaExpand(new tpd.TreeOps(term).appliedToArgs(tss.head)))
1666+
case _ => term
1667+
}
1668+
16671669
/** Convert `Type` to an `quoted.Type[T]` */
16681670
def QuotedType_seal(self: Type)(implicit ctx: Context): scala.quoted.Type[_] = {
16691671
val dummySpan = ctx.owner.span // FIXME
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
---
2+
layout: doc-page
3+
title: "Principled Meta Programming - Pattern matching"
4+
---
5+
6+
## Overview
7+
8+
### Quote patterns
9+
10+
```scala
11+
(expr: Expr[T]) match {
12+
case '{ pattern } =>
13+
}
14+
```
15+
16+
```scala
17+
(expr: Expr[T]) match {
18+
case '{ ... ${x: Expr[U]} ... } => x: Expr[U]
19+
case '{ ... ($x: U) ... } => x: Expr[U]
20+
}
21+
```
22+
23+
Quote matches exactly
24+
```scala
25+
(expr: Expr[T]) match {
26+
case '{ pattern } =>
27+
}
28+
```
29+
30+
```scala
31+
(expr: Expr[T]) match {
32+
case scala.runtime.quoted.Matcher.unapplySeq()(/*implicits*/ '{ pattern }, reflect) =>
33+
}
34+
```
35+
36+
37+
```scala
38+
(expr: Expr[T]) match {
39+
case '{ ... ${x: Expr[U]} ... } => x: Expr[U]
40+
case '{ ... ($x: U) ... } => x: Expr[U]
41+
}
42+
```
43+
44+
45+
```scala
46+
(expr: Expr[T]) match {
47+
case scala.runtime.quoted.Matcher.unapplySeq(..., x: Expr[U], ...)(/*implicits*/ '{ ... hole[U] ... }, reflect) =>
48+
}
49+
```
50+
51+
52+
53+
'[T] match {
54+
case '[Int] =>
55+
case '[Option[$t]] => '[List[$t]]
56+
}
57+
58+
59+
### Other expression patterns
60+
61+
```scala
62+
(expr: Expr[T]) match {
63+
case Literal(lit) => lit: T
64+
case IsValRef(expr2) => expr2: expr.type
65+
case IsDefRef(expr) => expr2: expr.type
66+
67+
case '{ ... ${Literal(lit)} ... }
68+
}
69+
```
70+
71+
72+
## Spec
73+
74+
### Quote pattern desugaring
75+
76+
Given a quote pattern `'{ pattern }` the desugaring is defined in terms of the runtime `<patern>` and the extracted bindings `E<pattern>`.
77+
```scala
78+
Matcher.unapplySeq(E<pattern>)(/*implicits*/ '{ <pattern> }, reflect)
79+
```
80+
81+
`<pattern>` is defined recursively in the following way:
82+
83+
| Quote pattern syntax | Matcher pattern | Extracted |
84+
| ------------- |:-------------:| ----------- |
85+
| `<$x>` where `x: Expr[X]` | `Matcher.hole[X]` | `x: Expr[T]` |
86+
| `<$t>` where `x: Type[X]` | `Matcher.HOLE[X]` | `t: Type[T]` |
87+
| `<$n: X>` where `n` is a name of `val`, `var`, `def` or parameter definition | `n: bindHole[X]` | `n: Binding[X]` |
88+
| `<if (e1) e2 else e3>` | `if (<e1>) <e2> else <e3>` | E<`e1`> ++ E<`e2`> ++ E<`e3`>) |
89+
| `<while (e1) e2>` | `while (<e1>) <e2>` | E<`e1`> ++ E<`e2`> |
90+
| `<f[T1, ..., Tn](e1, ..., em)>` | `<f>[<T1>, ..., <Tn>](<e1>, ..., <em>)` | E<`f`> ++ E<`T1`> ++ ... ++ E<`Tn`> ++ E<`e1`> ++ ... ++ E<`em`> |
91+
| `<(e1: T1, ..., en: Tn) => body>` | `(<e1: T1>, ..., <en: Tn>) => <body>` | E<`e1: T1`> ++ ... ++ E<`en: Tn`> ++ E<`body`> |
92+
| `<lhs = rhs>` | `lhs = <rhs>` | E<`rhs`> |
93+
| `<new X>` | `new <X>` | E<`X`> |
94+
| `<this>` | `this` | |
95+
| `<qual.super[T]>` | `<qual>.super[<T>]` | E<`qual`> ++ E<`T`) |
96+
| `<K[T1, ..., Tn]>` | `<K>[<T1>, ..., <Tn>]` | E<`K`> ++ E<`T1`> ++ ... ++ E<`Tn`> |
97+
| `<x>` where `x` is an identifier | `x` | |
98+
| `<T>` where `T` is an identifier | `T` | |
99+
100+
101+
### Quote pattern match runtime
102+
103+
| Match | Condition |
104+
| ------------- |:-------------:|
105+
| matches(`s`, `hole[T]`) for `s` of type `S` | `S <:< T` |
106+
| matches(`T`, `Hole[T]`) | |
107+
| matches(`n: X1`, `n: bindHole[X2]`) | matches(`X1`, `X2`) |
108+
| matches(`if (s1) s2 else s3`, `if (p1) p2 else p3`) | matches(`s1`, `p1`) && matches(`s2`, `p2`) && matches(`s3`,`p3`) |
109+
| matches(`while (s1) s2`, `while (p1) p2`) | matches(`s1`, `p1`) && matches(`s2`, `p2`) |
110+
111+
| Match | Result |
112+
| ------------- |:-------------:|
113+
| result(`s`, `hole[T]`) for `s` of type `S` | `s` as an `Expr[T]` |
114+
| result(`T`, `Hole[T]`) | `T` as a `Type[T]` |
115+
| matches(`n: X1`, `n: bindHole[X2]`) | (`n` as a `Binding[X2]`) ++ result(`X1`, `X2`) |
116+
| result(`if (s1) s2 else s3`, `if (p1) p2 else p3`) | result(`s1`, `p1`) ++ result(`s2`, `p2`) ++ result(`s3`,`p3`) |
117+
| result(`while (s1) s2`, `while (p1) p2`) | result(`s1`, `p1`) ++ result(`s2`, `p2`) |

library/src-bootstrapped/scala/quoted/Type.scala

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ sealed abstract class Type[T <: AnyKind] {
1313

1414
/** Some basic type tags, currently incomplete */
1515
object Type {
16-
1716
implicit def UnitTag: Type[Unit] = new TaggedType[Unit]
1817
implicit def BooleanTag: Type[Boolean] = new TaggedType[Boolean]
1918
implicit def ByteTag: Type[Byte] = new TaggedType[Byte]
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
package scala.runtime.quoted
2+
3+
import scala.quoted._
4+
import scala.quoted.matching.Binding
5+
6+
import scala.tasty._
7+
8+
object Matcher {
9+
10+
type Hole[T <: AnyKind] = T
11+
type bindHole[T] = T
12+
13+
def hole[T]: T = ???
14+
def seqHole[T <: Seq[_]]: T = ???
15+
16+
/**
17+
*
18+
* @param scrutineeExpr
19+
* @param patternExpr
20+
* @param reflection
21+
* @return None if it did not match, Some(seq) if it matched where seq contains Expr[_], Type[_], Binding[_] or Seq[Expr[_]]
22+
*/
23+
def unapplySeq(scrutineeExpr: Expr[_])(implicit patternExpr: Expr[_], reflection: Reflection): Option[Seq[Any]] = {
24+
import reflection._
25+
26+
def treeMatches(scrutinee: Tree, pattern: Tree): Option[Seq[Any]] = {
27+
import Term._
28+
import TypeTree.{Ident => TypeIdent, Select => TypeSelect, _}
29+
30+
(scrutinee, pattern) match {
31+
// Normalize blocks without statements
32+
case (Block(Nil, expr), _) => treeMatches(expr, pattern)
33+
case (_, Block(Nil, pat)) => treeMatches(scrutinee, pat)
34+
35+
case (IsTerm(scrutinee), TypeApply(Ident("hole"), tpt :: Nil))
36+
if pattern.symbol.fullName == "scala.runtime.quoted.Matcher$.hole" && // TODO check symbol equality instead of its name
37+
scrutinee.tpe <:< tpt.tpe =>
38+
Some(Seq(scrutinee.seal2))
39+
40+
case (IsTerm(scrutinee @ Term.Repeated(args, _)), TypeApply(Ident("seqHole"), tpt :: Nil))
41+
if pattern.symbol.fullName == "scala.runtime.quoted.Matcher$.seqHole" && // TODO check symbol equality instead of its name
42+
scrutinee.tpe <:< tpt.tpe =>
43+
Some(Seq(args.map(_.seal2).toSeq))
44+
45+
case (IsTypeTree(scrutinee), IsTypeTree(pattern @ TypeTree.Applied(TypeIdent("Hole"), IsTypeTree(tpt) :: Nil)))
46+
if pattern.symbol.fullName == "scala.runtime.quoted.Matcher$.Hole" && // TODO check symbol equality instead of its name
47+
scrutinee.tpe <:< tpt.tpe => // Is the subtype check required?
48+
Some(Seq(scrutinee))
49+
50+
case (ValDef(_, tpt1, rhs1), ValDef(_, bindHole @ TypeTree.Applied(TypeIdent("bindHole"), IsTypeTree(tpt2) :: Nil), rhs2))
51+
if bindHole.symbol.fullName == "scala.runtime.quoted.Matcher$.bindHole" && // TODO check symbol equality instead of its name
52+
tpt1.tpe <:< tpt2.tpe => // Is the subtype check required?
53+
54+
val sym = scrutinee.symbol
55+
val binding = new Binding(sym.name, sym)
56+
57+
def rhsMatchings = (rhs1, rhs2) match {
58+
case (Some(b1), Some(b2)) => treeMatches(b1, b2)
59+
case (None, None) => Some(Seq.empty)
60+
case _ => None
61+
}
62+
63+
foldMatchings(Some(Seq(binding)) :: treeMatches(tpt1, tpt2) :: rhsMatchings :: Nil)
64+
65+
case (Inlined(_, Nil, scr), _) =>
66+
treeMatches(scr, pattern)
67+
case (_, Inlined(_, Nil, pat)) =>
68+
treeMatches(scrutinee, pat)
69+
70+
case (Literal(constant1), Literal(constant2)) if constant1 == constant2 =>
71+
Some(Seq.empty)
72+
73+
case (Ident(_), Ident(_)) if scrutinee.symbol == pattern.symbol =>
74+
Some(Seq.empty)
75+
76+
case (Typed(expr1, tpt1), Typed(expr2, tpt2)) =>
77+
foldMatchings(treeMatches(expr1, expr2) :: treeMatches(tpt1, tpt2) :: Nil)
78+
79+
case (Select(qual1, _), Select(qual2, _)) if scrutinee.symbol == pattern.symbol =>
80+
treeMatches(qual1, qual2)
81+
82+
case (Ident(_), Select(_, _)) if scrutinee.symbol == pattern.symbol =>
83+
Some(Seq.empty)
84+
85+
case (Select(_, _), Ident(_)) if scrutinee.symbol == pattern.symbol =>
86+
Some(Seq.empty)
87+
88+
case (Apply(fn1, args1), Apply(fn2, args2)) if fn1.symbol == fn2.symbol =>
89+
foldMatchings(treeMatches(fn1, fn2) :: (for ((arg1, arg2) <- args1.zip(args2)) yield treeMatches(arg1, arg2)))
90+
91+
case (TypeApply(fn1, args1), TypeApply(fn2, args2)) if fn1.symbol == fn2.symbol =>
92+
foldMatchings(treeMatches(fn1, fn2) :: (for ((arg1, arg2) <- args1.zip(args2)) yield treeMatches(arg1, arg2)))
93+
94+
case (Block(stats1, expr1), Block(stats2, expr2)) =>
95+
// TODO handle bindings
96+
foldMatchings((for ((stat1, stat2) <- stats1.zip(stats2)) yield treeMatches(stat1, stat2)) ::: treeMatches(expr1, expr2) :: Nil)
97+
98+
case (If(cond1, thenp1, elsep1), If(cond2, thenp2, elsep2)) =>
99+
foldMatchings(treeMatches(cond1, cond2) :: treeMatches(thenp1, thenp2) :: treeMatches(elsep1, elsep2) :: Nil)
100+
101+
case (Assign(lhs1, rhs1), Assign(lhs2, rhs2)) =>
102+
// TODO how to handle LHS?
103+
if (treeMatches(lhs1, lhs2).isDefined) treeMatches(rhs1, rhs2)
104+
else None
105+
106+
case (While(cond1, body1), While(cond2, body2)) =>
107+
foldMatchings(treeMatches(cond1, cond2) :: treeMatches(body1, body2) :: Nil)
108+
109+
case (NamedArg(name1, expr1), NamedArg(name2, expr2)) if name1 == name2 =>
110+
treeMatches(expr1, expr2)
111+
112+
case (New(tpt1), New(tpt2)) =>
113+
treeMatches(tpt1, tpt2)
114+
115+
case (This(_), This(_)) if scrutinee.symbol == pattern.symbol =>
116+
Some(Seq.empty)
117+
118+
case (Super(qual1, mix1), Super(qual2, mix2)) if mix1 == mix2 =>
119+
treeMatches(qual1, qual2)
120+
121+
case (Repeated(elems1, _), Repeated(elems2, _)) if elems1.size == elems2.size =>
122+
foldMatchings(for ((elem1, elem2) <- elems1.zip(elems2)) yield treeMatches(elem1, elem2))
123+
124+
case (IsTypeTree(scrutinee @ TypeIdent(_)), IsTypeTree(pattern @ TypeIdent(_))) if scrutinee.symbol == pattern.symbol =>
125+
Some(Seq.empty)
126+
127+
case (IsInferred(scrutinee), IsInferred(pattern)) if scrutinee.tpe <:< pattern.tpe =>
128+
Some(Seq.empty)
129+
130+
case (Applied(tycon1, args1), Applied(tycon2, args2)) =>
131+
val matchings: List[Option[Seq[Any]]] =
132+
treeMatches(tycon1, tycon2) :: (for ((arg1, arg2) <- args1.zip(args2)) yield treeMatches(arg1, arg2))
133+
foldMatchings(matchings)
134+
135+
case (DefDef(_, typeParams1, paramss1, returnTpt1, Some(rhs1)), DefDef(_, typeParams2, paramss2, returnTpt2, Some(rhs2))) =>
136+
val matchings: List[Option[Seq[Any]]] =
137+
for ((tree1: Tree, tree2: Tree) <- (typeParams1 ::: paramss1.flatten ::: rhs1 :: Nil).zip(typeParams2 ::: paramss2.flatten ::: rhs2 :: Nil)) yield treeMatches(tree1, tree2)
138+
foldMatchings(matchings)
139+
140+
case (Term.Lambda(_, tpt1), Term.Lambda(_, tpt2)) =>
141+
// TODO match tpt1 with tpt2?
142+
Some(Seq.empty)
143+
144+
case _ =>
145+
// println(
146+
// s""">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
147+
// |Scrutinee
148+
// | ${scrutinee.showCode}
149+
// |
150+
// |${scrutinee.show}
151+
// |
152+
// |did not match pattern
153+
// | ${pattern.showCode}
154+
// |
155+
// |${pattern.show}
156+
// |
157+
// |
158+
// |
159+
// |
160+
// |""".stripMargin)
161+
None
162+
}
163+
}
164+
165+
treeMatches(scrutineeExpr.unseal, patternExpr.unseal)
166+
}
167+
168+
private def foldMatchings(matchings: List[Option[Seq[Any]]]): Option[Seq[Any]] = {
169+
matchings.foldLeft[Option[Seq[Any]]](Some(Seq.empty)) {
170+
case (Some(acc), Some(holes)) => Some(acc ++ holes)
171+
case (_, _) => None
172+
}
173+
}
174+
175+
}

library/src-bootstrapped/scala/tasty/reflect/QuotedOps.scala

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ trait QuotedOps extends Core {
1919
/** Convert `Term` to an `Expr[T]` and check that it conforms to `T` */
2020
def seal[T](implicit tpe: scala.quoted.Type[T], ctx: Context): scala.quoted.Expr[T] =
2121
kernel.QuotedExpr_seal(term)(tpe)
22+
23+
def seal2(implicit ctx: Context): scala.quoted.Expr[_] =
24+
kernel.QuotedExpr_seal2(term)
2225
}
2326

2427
implicit class TypeToQuotedAPI(tpe: Type) {
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package scala.runtime.quoted
2+
3+
import scala.quoted._
4+
import scala.tasty._
5+
6+
object Matcher {
7+
8+
type Hole[T /* <: AnyKind */] = T
9+
10+
def hole[T]: T = ???
11+
def literal[T]: T = ???
12+
13+
def unapplySeq(scrutineeExpr: Expr[_])(implicit patternExpr: Expr[_], reflection: Reflection): Option[Seq[Any]] =
14+
throw new Exception("running on non bootstrapped library")
15+
16+
}

0 commit comments

Comments
 (0)