Skip to content

Commit 5938d65

Browse files
committed
Support type variable definitions in quoted patterns
Support explicit type variable definition in quoted patterns. This allows users to set explicit bounds or use the binding twice. Previously this was only possible on quoted expression patterns case '{ ... }. ```scala case '[type x; x] => case '[type x; Map[x, x]] => case '[type x <: List[Any]; x] => case '[type f[X]; f] => case '[type f <: AnyKind; f] => ``` Fixes #10864 Fixes #11738
1 parent 1a5465e commit 5938d65

File tree

16 files changed

+223
-13
lines changed

16 files changed

+223
-13
lines changed

compiler/src/dotty/tools/dotc/ast/Desugar.scala

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -366,7 +366,7 @@ object desugar {
366366
/** Split out the quoted pattern type variable definition from the pattern.
367367
*
368368
* Type variable definitions are all the `type t` defined at the start of a quoted pattern.
369-
* Were name `t` is a pattern type variable name (i.e. lower case letters).
369+
* Where name `t` is a pattern type variable name (i.e. lower case letters).
370370
*
371371
* ```
372372
* type t1; ...; type tn; <pattern>
@@ -379,16 +379,12 @@ object desugar {
379379
def quotedPatternTypeVariables(tree: untpd.Tree)(using Context): (List[untpd.TypeDef], untpd.Tree) =
380380
tree match
381381
case untpd.Block(stats, expr) =>
382-
val untpdTypeVariables = stats.takeWhile {
383-
case tdef @ untpd.TypeDef(name, _) => name.isVarPattern
384-
case _ => false
385-
}.asInstanceOf[List[untpd.TypeDef]]
386-
val otherStats = stats.dropWhile {
382+
val (untpdTypeVariables, otherStats) = stats.span {
387383
case tdef @ untpd.TypeDef(name, _) => name.isVarPattern
388384
case _ => false
389385
}
390386
val pattern = if otherStats.isEmpty then expr else untpd.cpy.Block(tree)(otherStats, expr)
391-
(untpdTypeVariables, pattern)
387+
(untpdTypeVariables.asInstanceOf[List[untpd.TypeDef]], pattern)
392388
case _ =>
393389
(Nil, tree)
394390

compiler/src/dotty/tools/dotc/parsing/Parsers.scala

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1740,6 +1740,27 @@ object Parsers {
17401740
/** The block in a quote or splice */
17411741
def stagedBlock() = inBraces(block(simplify = true))
17421742

1743+
/** TypeBlock ::= {TypeBlockStat semi} Type
1744+
*/
1745+
def typeBlock(): Tree =
1746+
typeBlockStats() match
1747+
case Nil => typ()
1748+
case tdefs => Block(tdefs, typ())
1749+
1750+
def typeBlockStats(): List[Tree] =
1751+
val tdefs = new ListBuffer[Tree]
1752+
while in.token == TYPE do tdefs += typeBlockStat()
1753+
tdefs.toList
1754+
1755+
/** TypeBlockStat ::= ‘type’ {nl} TypeDcl
1756+
*/
1757+
def typeBlockStat(): Tree =
1758+
val mods = defAnnotsMods(BitSet())
1759+
val tdef = typeDefOrDcl(in.offset, in.skipToken(mods))
1760+
if in.token == SEMI then in.nextToken()
1761+
if in.isNewLine then in.nextToken()
1762+
tdef
1763+
17431764
/** ExprSplice ::= ‘$’ spliceId -- if inside quoted block
17441765
* | ‘$’ ‘{’ Block ‘}’ -- unless inside quoted pattern
17451766
* | ‘$’ ‘{’ Pattern ‘}’ -- when inside quoted pattern
@@ -2480,7 +2501,7 @@ object Parsers {
24802501
atSpan(in.skipToken()) {
24812502
withinStaged(StageKind.Quoted | (if (location.inPattern) StageKind.QuotedPattern else 0)) {
24822503
val body =
2483-
if (in.token == LBRACKET) inBrackets(typ())
2504+
if (in.token == LBRACKET) inBrackets(typeBlock())
24842505
else stagedBlock()
24852506
Quote(body, Nil)
24862507
}
@@ -3758,6 +3779,8 @@ object Parsers {
37583779
else makeTypeDef(bounds)
37593780
case SEMI | NEWLINE | NEWLINES | COMMA | RBRACE | OUTDENT | EOF =>
37603781
makeTypeDef(typeBounds())
3782+
case _ if (staged & StageKind.QuotedPattern) != 0 =>
3783+
makeTypeDef(typeBounds())
37613784
case _ =>
37623785
syntaxErrorOrIncomplete(ExpectedTypeBoundOrEquals(in.token))
37633786
return EmptyTree // return to avoid setting the span to EmptyTree

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

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ trait QuotesAndSplices {
166166
tree.srcPos,
167167
"\n\nSIP-53: https://docs.scala-lang.org/sips/quote-pattern-type-variable-syntax.html")
168168
if !(typeSymInfo =:= TypeBounds.empty) && !(typeSym.info <:< typeSymInfo) then
169-
report.warning(em"Ignored bound$typeSymInfo\n\nConsider defining bounds explicitly `'{ $typeSym$typeSymInfo; ... }`", tree.srcPos)
169+
report.warning(em"Ignored bound$typeSymInfo\n\nConsider defining bounds explicitly `'{ $typeSym${typeSym.info & typeSymInfo}; ... }`", tree.srcPos)
170170
ref(typeSym)
171171
case None =>
172172
def spliceOwner(ctx: Context): Symbol =
@@ -399,6 +399,17 @@ trait QuotesAndSplices {
399399
}
400400
val (untpdTypeVariables, quoted0) = desugar.quotedPatternTypeVariables(desugar.quotedPattern(quoted, untpd.TypedSplice(TypeTree(quotedPt))))
401401

402+
for tdef @ untpd.TypeDef(_, rhs) <- untpdTypeVariables do rhs match
403+
case _: TypeBoundsTree => // ok
404+
case LambdaTypeTree(_, body: TypeBoundsTree) => // ok
405+
case _ => report.error("Quote type variable definition cannot be an alias", tdef.srcPos)
406+
407+
if quoted.isType && untpdTypeVariables.nonEmpty then
408+
checkExperimentalFeature(
409+
"explicit type variable declarations quoted type patterns (SIP-53)",
410+
untpdTypeVariables.head.srcPos,
411+
"\n\nSIP-53: https://docs.scala-lang.org/sips/quote-pattern-type-variable-syntax.html")
412+
402413
val (typeTypeVariables, patternCtx) =
403414
val quoteCtx = quotePatternContext()
404415
if untpdTypeVariables.isEmpty then (Nil, quoteCtx)
@@ -409,7 +420,7 @@ trait QuotesAndSplices {
409420
addQuotedPatternTypeVariable(typeVariable.symbol)
410421

411422
val pattern =
412-
if quoted.isType then typedType(quoted0, WildcardType)
423+
if quoted.isType then typedType(quoted0, WildcardType)(using patternCtx)
413424
else typedExpr(quoted0, WildcardType)
414425

415426
if untpdTypeVariables.isEmpty then pattern

docs/_docs/reference/metaprogramming/macros.md

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -530,15 +530,24 @@ It works the same way as a quoted pattern but is restricted to contain a type.
530530
Type variables can be used in quoted type patterns to extract a type.
531531

532532
```scala
533-
def empty[T: Type]: Expr[T] =
533+
def empty[T: Type](using Quotes): Expr[T] =
534534
Type.of[T] match
535535
case '[String] => '{ "" }
536536
case '[List[t]] => '{ List.empty[t] }
537+
case '[type t <: Option[Int]; List[t]] => '{ List.empty[t] }
537538
...
538539
```
539-
540540
`Type.of[T]` is used to summon the given instance of `Type[T]` in scope, it is equivalent to `summon[Type[T]]`.
541541

542+
It is possible to match against a higher-kinded type using appropriate type bounds on type variables.
543+
```scala
544+
def empty[K <: AnyKind : Type](using Quotes): Type[?] =
545+
Type.of[K] match
546+
case '[type f[X]; f] => Type.of[f]
547+
case '[type f[X <: Int, Y]; f] => Type.of[f]
548+
case '[type k <: AnyKind; k ] => Type.of[k]
549+
```
550+
542551
#### Type testing and casting
543552
It is important to note that instance checks and casts on `Expr`, such as `isInstanceOf[Expr[T]]` and `asInstanceOf[Expr[T]]`, will only check if the instance is of the class `Expr` but will not be able to check the `T` argument.
544553
These cases will issue a warning at compile-time, but if they are ignored, they can result in unexpected behavior.

docs/_docs/reference/syntax.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -274,7 +274,7 @@ ColonArgument ::= colon [LambdaStart]
274274
LambdaStart ::= FunParams (‘=>’ | ‘?=>’)
275275
| HkTypeParamClause ‘=>’
276276
Quoted ::= ‘'’ ‘{’ Block ‘}’
277-
| ‘'’ ‘[’ Type ‘]’
277+
| ‘'’ ‘[’ TypeBlock ‘]’
278278
ExprSplice ::= spliceId -- if inside quoted block
279279
| ‘$’ ‘{’ Block ‘}’ -- unless inside quoted pattern
280280
| ‘$’ ‘{’ Pattern ‘}’ -- when inside quoted pattern
@@ -293,6 +293,8 @@ BlockStat ::= Import
293293
| Extension
294294
| Expr1
295295
| EndMarker
296+
TypeBlock ::= {TypeBlockStat semi} Type
297+
TypeBlockStat ::= ‘type’ {nl} TypeDcl
296298
297299
ForExpr ::= ‘for’ ‘(’ Enumerators0 ‘)’ {nl} [‘do‘ | ‘yield’] Expr
298300
| ‘for’ ‘{’ Enumerators0 ‘}’ {nl} [‘do‘ | ‘yield’] Expr
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import scala.quoted.*
2+
3+
def empty[K <: AnyKind : Type](using Quotes): Type[?] =
4+
Type.of[K] match
5+
case '[type t; `t`] => Type.of[t] // error
6+
case '[type f[X]; `f`] => Type.of[f] // error
7+
case '[type f[X <: Int, Y]; `f`] => Type.of[f] // error
8+
case '[type k <: AnyKind; `k` ] => Type.of[k] // error
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import scala.quoted.*
2+
def types(t: Type[?])(using Quotes) = t match {
3+
case '[ type t; Int ] =>
4+
case '[ type t <: Int; Int ] =>
5+
case '[ type t >: 1 <: Int; Int ] =>
6+
case '[ type t = Int; Int ] => // error
7+
case '[ type t = scala.Int; Int ] => // error
8+
case '[ type f[t] <: List[Any]; Int ] =>
9+
case '[ type f[t <: Int] <: List[Any]; Int ] =>
10+
case '[ type f[t] = List[Any]; Int ] => // error
11+
}
12+
13+
def expressions(x: Expr[Any])(using Quotes) = x match {
14+
case '{ type t; () } =>
15+
case '{ type t <: Int; () } =>
16+
case '{ type t >: 1 <: Int; () } =>
17+
case '{ type t = Int; () } => // error
18+
case '{ type t = scala.Int; () } => // error
19+
case '{ type f[t] <: List[Any]; () } =>
20+
case '{ type f[t <: Int] <: List[Any]; () } =>
21+
case '{ type f[t] = List[Any]; () } => // error
22+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
-- Warning: tests/neg-macros/quote-type-variable-no-inference.scala:5:17 -----------------------------------------------
2+
5 | case '[ F[t, t] ] => // warn // error
3+
| ^
4+
| Ignored bound <: Double
5+
|
6+
| Consider defining bounds explicitly `'{ type t <: Int & Double; ... }`
7+
-- [E057] Type Mismatch Error: tests/neg-macros/quote-type-variable-no-inference.scala:5:15 ----------------------------
8+
5 | case '[ F[t, t] ] => // warn // error
9+
| ^
10+
| Type argument t does not conform to upper bound Double
11+
|
12+
| longer explanation available when compiling with `-explain`
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import scala.quoted.*
2+
3+
def test(x: Type[?])(using Quotes) =
4+
x match
5+
case '[ F[t, t] ] => // warn // error
6+
case '[ type u <: Int & Double; F[u, u] ] =>
7+
8+
type F[x <: Int, y <: Double]

tests/pos-macros/i10864/Macro_1.scala

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import scala.quoted._
2+
3+
case class T(t: Type[_])
4+
5+
object T {
6+
def impl[T <: AnyKind](using tt: Type[T])(using Quotes): Expr[Unit] = {
7+
val t = T(tt)
8+
t.t match
9+
case '[type x <: AnyKind; x] => // ok
10+
case _ => quotes.reflect.report.error("not ok :(")
11+
'{}
12+
}
13+
14+
inline def run[T <: AnyKind] = ${ impl[T] }
15+
}

tests/pos-macros/i10864/Test_2.scala

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
def test =
2+
T.run[List]
3+
T.run[Map]
4+
T.run[Tuple22]
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import scala.quoted._
2+
3+
case class T(t: Type[_])
4+
5+
object T {
6+
def impl[T <: AnyKind](using tt: Type[T])(using Quotes): Expr[Unit] = {
7+
val t = T(tt)
8+
t.t match
9+
case '[type x; x] =>
10+
assert(Type.show[x] == "scala.Int", Type.show[x])
11+
case '[type f[X]; f] =>
12+
assert(Type.show[f] == "[A >: scala.Nothing <: scala.Any] => scala.collection.immutable.List[A]", Type.show[f])
13+
case '[type f[X <: Int]; f] =>
14+
assert(Type.show[f] == "[T >: scala.Nothing <: scala.Int] => C[T]", Type.show[f])
15+
case '[type f <: AnyKind; f] =>
16+
assert(Type.show[f] == "[K >: scala.Nothing <: scala.Any, V >: scala.Nothing <: scala.Any] => scala.collection.immutable.Map[K, V]", Type.show[f])
17+
'{}
18+
}
19+
20+
inline def run[T <: AnyKind] = ${ impl[T] }
21+
}

tests/pos-macros/i10864a/Test_2.scala

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
@main
2+
def run =
3+
T.run[Int]
4+
T.run[C]
5+
T.run[List]
6+
T.run[Map]
7+
8+
class C[T <: Int]

tests/pos-macros/i11738.scala

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import scala.quoted.*
2+
3+
def blah[A](using Quotes, Type[A]): Expr[Unit] =
4+
Type.of[A] match
5+
case '[h *: t] => println(s"h = ${Type.show[h]}, t = ${Type.show[t]}") // ok
6+
case '[type f[X]; f[a]] => println(s"f = ${Type.show[f]}, a = ${Type.show[a]}") // error
7+
case _ =>
8+
'{()}

tests/pos-macros/i7264.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@ class Foo {
33
def f[T2](t: Type[T2])(using Quotes) = t match {
44
case '[ *:[Int, t2] ] =>
55
Type.of[ *:[Int, t2] ]
6+
case '[ type t <: Tuple; *:[t, t] ] =>
67
}
78
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import scala.quoted.*
2+
def types(t: Type[?])(using Quotes) = t match {
3+
case '[
4+
type t;
5+
t
6+
] =>
7+
8+
case '[
9+
type t
10+
t
11+
] =>
12+
13+
case '[
14+
type t
15+
List[t]
16+
] =>
17+
18+
case '[
19+
type t;
20+
type u;
21+
Map[t, u]
22+
] =>
23+
24+
case '[
25+
type t
26+
type u
27+
Map[t, u]
28+
] =>
29+
30+
case '[
31+
type t; type u
32+
t => u
33+
] =>
34+
}
35+
36+
def expressions(x: Expr[Any])(using Quotes) = x match {
37+
case '{
38+
type t;
39+
$x: t
40+
} =>
41+
42+
case '{
43+
type t
44+
$x: t
45+
} =>
46+
47+
case '{
48+
type t;
49+
List()
50+
} =>
51+
52+
case '{
53+
type t
54+
List()
55+
} =>
56+
57+
case '{
58+
type t
59+
type u
60+
Map.empty[t, u]
61+
} =>
62+
}

0 commit comments

Comments
 (0)