Skip to content

Commit 11c4bca

Browse files
authored
SIP-53 - Quote pattern explicit type variable syntax (#17362)
SIP-53: https://github.com/scala/improvement-proposals/blob/main/content/quote-pattern-type-variable-syntax.md This PR implements the feature under experimental mode.
2 parents ddb632b + 86b7110 commit 11c4bca

19 files changed

+353
-39
lines changed

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

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,31 @@ object desugar {
363363
adaptToExpectedTpt(tree)
364364
}
365365

366+
/** Split out the quoted pattern type variable definition from the pattern.
367+
*
368+
* Type variable definitions are all the `type t` defined at the start of a quoted pattern.
369+
* Where name `t` is a pattern type variable name (i.e. lower case letters).
370+
*
371+
* ```
372+
* type t1; ...; type tn; <pattern>
373+
* ```
374+
* is split into
375+
* ```
376+
* (List(<type t1>; ...; <type tn>), <pattern>)
377+
* ```
378+
*/
379+
def quotedPatternTypeVariables(tree: untpd.Tree)(using Context): (List[untpd.TypeDef], untpd.Tree) =
380+
tree match
381+
case untpd.Block(stats, expr) =>
382+
val (untpdTypeVariables, otherStats) = stats.span {
383+
case tdef @ untpd.TypeDef(name, _) => name.isVarPattern
384+
case _ => false
385+
}
386+
val pattern = if otherStats.isEmpty then expr else untpd.cpy.Block(tree)(otherStats, expr)
387+
(untpdTypeVariables.asInstanceOf[List[untpd.TypeDef]], pattern)
388+
case _ =>
389+
(Nil, tree)
390+
366391
/** Add all evidence parameters in `params` as implicit parameters to `meth`.
367392
* If the parameters of `meth` end in an implicit parameter list or using clause,
368393
* evidence parameters are added in front of that list. Otherwise they are added

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

Lines changed: 38 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1737,8 +1737,39 @@ object Parsers {
17371737
})
17381738
else t
17391739

1740-
/** The block in a quote or splice */
1741-
def stagedBlock() = inBraces(block(simplify = true))
1740+
/** TypeBlock ::= {TypeBlockStat semi} Type
1741+
*/
1742+
def typeBlock(): Tree =
1743+
typeBlockStats() match
1744+
case Nil => typ()
1745+
case tdefs => Block(tdefs, typ())
1746+
1747+
def typeBlockStats(): List[Tree] =
1748+
val tdefs = new ListBuffer[Tree]
1749+
while in.token == TYPE do tdefs += typeBlockStat()
1750+
tdefs.toList
1751+
1752+
/** TypeBlockStat ::= ‘type’ {nl} TypeDcl
1753+
*/
1754+
def typeBlockStat(): Tree =
1755+
val mods = defAnnotsMods(BitSet())
1756+
val tdef = typeDefOrDcl(in.offset, in.skipToken(mods))
1757+
if in.token == SEMI then in.nextToken()
1758+
if in.isNewLine then in.nextToken()
1759+
tdef
1760+
1761+
/** Quoted ::= ‘'’ ‘{’ Block ‘}’
1762+
* | ‘'’ ‘[’ TypeBlock ‘]’
1763+
*/
1764+
def quote(inPattern: Boolean): Tree =
1765+
atSpan(in.skipToken()) {
1766+
withinStaged(StageKind.Quoted | (if (inPattern) StageKind.QuotedPattern else 0)) {
1767+
val body =
1768+
if (in.token == LBRACKET) inBrackets(typeBlock())
1769+
else inBraces(block(simplify = true))
1770+
Quote(body, Nil)
1771+
}
1772+
}
17421773

17431774
/** ExprSplice ::= ‘$’ spliceId -- if inside quoted block
17441775
* | ‘$’ ‘{’ Block ‘}’ -- unless inside quoted pattern
@@ -1754,7 +1785,8 @@ object Parsers {
17541785
val expr =
17551786
if (in.name.length == 1) {
17561787
in.nextToken()
1757-
withinStaged(StageKind.Spliced)(if (inPattern) inBraces(pattern()) else stagedBlock())
1788+
val inPattern = (staged & StageKind.QuotedPattern) != 0
1789+
withinStaged(StageKind.Spliced)(inBraces(if inPattern then pattern() else block(simplify = true)))
17581790
}
17591791
else atSpan(in.offset + 1) {
17601792
val id = Ident(in.name.drop(1))
@@ -2477,14 +2509,7 @@ object Parsers {
24772509
canApply = false
24782510
blockExpr()
24792511
case QUOTE =>
2480-
atSpan(in.skipToken()) {
2481-
withinStaged(StageKind.Quoted | (if (location.inPattern) StageKind.QuotedPattern else 0)) {
2482-
val body =
2483-
if (in.token == LBRACKET) inBrackets(typ())
2484-
else stagedBlock()
2485-
Quote(body, Nil)
2486-
}
2487-
}
2512+
quote(location.inPattern)
24882513
case NEW =>
24892514
canApply = false
24902515
newExpr()
@@ -3760,6 +3785,8 @@ object Parsers {
37603785
else makeTypeDef(bounds)
37613786
case SEMI | NEWLINE | NEWLINES | COMMA | RBRACE | OUTDENT | EOF =>
37623787
makeTypeDef(typeBounds())
3788+
case _ if (staged & StageKind.QuotedPattern) != 0 =>
3789+
makeTypeDef(typeBounds())
37633790
case _ =>
37643791
syntaxErrorOrIncomplete(ExpectedTypeBoundOrEquals(in.token))
37653792
return EmptyTree // return to avoid setting the span to EmptyTree

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

Lines changed: 74 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,18 @@ import dotty.tools.dotc.transform.SymUtils._
2020
import dotty.tools.dotc.typer.ErrorReporting.errorTree
2121
import dotty.tools.dotc.typer.Implicits._
2222
import dotty.tools.dotc.typer.Inferencing._
23+
import dotty.tools.dotc.util.Property
2324
import dotty.tools.dotc.util.Spans._
2425
import dotty.tools.dotc.util.Stats.record
2526
import dotty.tools.dotc.reporting.IllegalVariableInPatternAlternative
2627
import scala.collection.mutable
2728

28-
2929
/** Type quotes `'{ ... }` and splices `${ ... }` */
3030
trait QuotesAndSplices {
3131
self: Typer =>
3232

33-
import tpd._
33+
import tpd.*
34+
import QuotesAndSplices.*
3435

3536
/** Translate `'{ e }` into `scala.quoted.Expr.apply(e)` and `'[T]` into `scala.quoted.Type.apply[T]`
3637
* while tracking the quotation level in the context.
@@ -155,19 +156,30 @@ trait QuotesAndSplices {
155156
* The resulting pattern is the split in `splitQuotePattern`.
156157
*/
157158
def typedQuotedTypeVar(tree: untpd.Ident, pt: Type)(using Context): Tree =
158-
def spliceOwner(ctx: Context): Symbol =
159-
if (ctx.mode.is(Mode.QuotedPattern)) spliceOwner(ctx.outer) else ctx.owner
160-
val name = tree.name.toTypeName
161-
val nameOfSyntheticGiven = PatMatGivenVarName.fresh(tree.name.toTermName)
162-
val expr = untpd.cpy.Ident(tree)(nameOfSyntheticGiven)
163159
val typeSymInfo = pt match
164160
case pt: TypeBounds => pt
165161
case _ => TypeBounds.empty
166-
val typeSym = newSymbol(spliceOwner(ctx), name, EmptyFlags, typeSymInfo, NoSymbol, tree.span)
167-
typeSym.addAnnotation(Annotation(New(ref(defn.QuotedRuntimePatterns_patternTypeAnnot.typeRef)).withSpan(tree.span)))
168-
val pat = typedPattern(expr, defn.QuotedTypeClass.typeRef.appliedTo(typeSym.typeRef))(
169-
using spliceContext.retractMode(Mode.QuotedPattern).withOwner(spliceOwner(ctx)))
170-
pat.select(tpnme.Underlying)
162+
getQuotedPatternTypeVariable(tree.name.asTypeName) match
163+
case Some(typeSym) =>
164+
checkExperimentalFeature(
165+
"support for multiple references to the same type (without backticks) in quoted type patterns (SIP-53)",
166+
tree.srcPos,
167+
"\n\nSIP-53: https://docs.scala-lang.org/sips/quote-pattern-type-variable-syntax.html")
168+
if !(typeSymInfo =:= TypeBounds.empty) && !(typeSym.info <:< typeSymInfo) then
169+
report.warning(em"Ignored bound$typeSymInfo\n\nConsider defining bounds explicitly `'{ $typeSym${typeSym.info & typeSymInfo}; ... }`", tree.srcPos)
170+
ref(typeSym)
171+
case None =>
172+
def spliceOwner(ctx: Context): Symbol =
173+
if (ctx.mode.is(Mode.QuotedPattern)) spliceOwner(ctx.outer) else ctx.owner
174+
val name = tree.name.toTypeName
175+
val nameOfSyntheticGiven = PatMatGivenVarName.fresh(tree.name.toTermName)
176+
val expr = untpd.cpy.Ident(tree)(nameOfSyntheticGiven)
177+
val typeSym = newSymbol(spliceOwner(ctx), name, EmptyFlags, typeSymInfo, NoSymbol, tree.span)
178+
typeSym.addAnnotation(Annotation(New(ref(defn.QuotedRuntimePatterns_patternTypeAnnot.typeRef)).withSpan(tree.span)))
179+
addQuotedPatternTypeVariable(typeSym)
180+
val pat = typedPattern(expr, defn.QuotedTypeClass.typeRef.appliedTo(typeSym.typeRef))(
181+
using spliceContext.retractMode(Mode.QuotedPattern).withOwner(spliceOwner(ctx)))
182+
pat.select(tpnme.Underlying)
171183

172184
private def checkSpliceOutsideQuote(tree: untpd.Tree)(using Context): Unit =
173185
if (level == 0 && !ctx.owner.ownersIterator.exists(_.isInlineMethod))
@@ -385,11 +397,35 @@ trait QuotesAndSplices {
385397
case Some(argPt: ValueType) => argPt // excludes TypeBounds
386398
case _ => defn.AnyType
387399
}
388-
val quoted0 = desugar.quotedPattern(quoted, untpd.TypedSplice(TypeTree(quotedPt)))
389-
val quoteCtx = quoteContext.addMode(Mode.QuotedPattern).retractMode(Mode.Pattern)
390-
val quoted1 =
391-
if quoted.isType then typedType(quoted0, WildcardType)(using quoteCtx)
392-
else typedExpr(quoted0, WildcardType)(using quoteCtx)
400+
val (untpdTypeVariables, quoted0) = desugar.quotedPatternTypeVariables(desugar.quotedPattern(quoted, untpd.TypedSplice(TypeTree(quotedPt))))
401+
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+
413+
val (typeTypeVariables, patternCtx) =
414+
val quoteCtx = quotePatternContext()
415+
if untpdTypeVariables.isEmpty then (Nil, quoteCtx)
416+
else typedBlockStats(untpdTypeVariables)(using quoteCtx)
417+
418+
val quoted1 = inContext(patternCtx) {
419+
for typeVariable <- typeTypeVariables do
420+
addQuotedPatternTypeVariable(typeVariable.symbol)
421+
422+
val pattern =
423+
if quoted.isType then typedType(quoted0, WildcardType)(using patternCtx)
424+
else typedExpr(quoted0, WildcardType)
425+
426+
if untpdTypeVariables.isEmpty then pattern
427+
else tpd.Block(typeTypeVariables, pattern)
428+
}
393429

394430
val (typeBindings, shape, splices) = splitQuotePattern(quoted1)
395431

@@ -446,3 +482,24 @@ trait QuotesAndSplices {
446482
proto = quoteClass.typeRef.appliedTo(replaceBindings(quoted1.tpe)))
447483
}
448484
}
485+
486+
object QuotesAndSplices {
487+
import tpd._
488+
489+
/** Key for mapping from quoted pattern type variable names into their symbol */
490+
private val TypeVariableKey = new Property.Key[collection.mutable.Map[TypeName, Symbol]]
491+
492+
/** Get the symbol for the quoted pattern type variable if it exists */
493+
def getQuotedPatternTypeVariable(name: TypeName)(using Context): Option[Symbol] =
494+
ctx.property(TypeVariableKey).get.get(name)
495+
496+
/** Get the symbol for the quoted pattern type variable if it exists */
497+
def addQuotedPatternTypeVariable(sym: Symbol)(using Context): Unit =
498+
ctx.property(TypeVariableKey).get.update(sym.name.asTypeName, sym)
499+
500+
/** Context used to type the contents of a quoted */
501+
def quotePatternContext()(using Context): Context =
502+
quoteContext.fresh.setNewScope
503+
.addMode(Mode.QuotedPattern).retractMode(Mode.Pattern)
504+
.setProperty(TypeVariableKey, collection.mutable.Map.empty)
505+
}

docs/_docs/reference/metaprogramming/macros.md

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -504,18 +504,22 @@ def let(x: Expr[Any])(using Quotes): Expr[Any] =
504504
let('{1}) // will return a `Expr[Any]` that contains an `Expr[Int]]`
505505
```
506506

507+
It is also possible to refer to the same type variable multiple times in a pattern.
508+
509+
```scala
510+
case '{ $x: (t, t) } =>
511+
```
512+
507513
While we can define the type variable in the middle of the pattern, their normal form is to define them as a `type` with a lower case name at the start of the pattern.
508-
We use the Scala backquote `` `t` `` naming convention which interprets the string within the backquote as a literal name identifier.
509-
This is typically used when we have names that contain special characters that are not allowed for normal Scala identifiers.
510-
But we use it to explicitly state that this is a reference to that name and not the introduction of a new variable.
514+
511515
```scala
512-
case '{ type t; $x: `t` } =>
516+
case '{ type t; $x: t } =>
513517
```
514-
This is a bit more verbose but has some expressivity advantages such as allowing to define bounds on the variables and be able to refer to them several times in any scope of the pattern.
518+
519+
This is a bit more verbose but has some expressivity advantages such as allowing to define bounds on the variables.
515520

516521
```scala
517-
case '{ type t >: List[Int] <: Seq[Int]; $x: `t` } =>
518-
case '{ type t; $x: (`t`, `t`) } =>
522+
case '{ type t >: List[Int] <: Seq[Int]; $x: t } =>
519523
```
520524

521525

@@ -526,15 +530,24 @@ It works the same way as a quoted pattern but is restricted to contain a type.
526530
Type variables can be used in quoted type patterns to extract a type.
527531

528532
```scala
529-
def empty[T: Type]: Expr[T] =
533+
def empty[T: Type](using Quotes): Expr[T] =
530534
Type.of[T] match
531535
case '[String] => '{ "" }
532536
case '[List[t]] => '{ List.empty[t] }
537+
case '[type t <: Option[Int]; List[t]] => '{ List.empty[t] }
533538
...
534539
```
535-
536540
`Type.of[T]` is used to summon the given instance of `Type[T]` in scope, it is equivalent to `summon[Type[T]]`.
537541

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+
538551
#### Type testing and casting
539552
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.
540553
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: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import scala.quoted.*
2+
3+
def foo(using Quotes): Unit =
4+
(??? : Type[?]) match
5+
case '[ (t, t, t) ] => // error // error
6+
'{ ??? : Any } match
7+
case '{ type u; $x: u } => // error
8+
case '{ type u; ($ls: List[u]).map($f: u => Int) } => // error // error
9+
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/neg-macros/quotedPatterns-5.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import scala.quoted.*
22
object Test {
33
def test(x: quoted.Expr[Int])(using Quotes): Unit = x match {
44
case '{ type t; 4 } => Type.of[t]
5-
case '{ type t; poly[t]($x); 4 } => // error: duplicate pattern variable: t
5+
case '{ type t; poly[t]($x); 4 } =>
66
case '{ type `t`; poly[`t`]($x); 4 } =>
77
Type.of[t] // error
88
case _ =>

0 commit comments

Comments
 (0)