Skip to content

Commit

Permalink
Quoted pattern type variables without backticks
Browse files Browse the repository at this point in the history
With this change we can support references to quote pattern type variables without backticks.

```scala
case '{ type t; ... : F[t] }
```
SIP: https://github.com/scala/improvement-proposals/blob/main/content/quote-pattern-type-variable-syntax.md?plain=1#L66-L70

```scala
case '{ ... : F[t, t] }
```

SIP: https://github.com/scala/improvement-proposals/blob/main/content/quote-pattern-type-variable-syntax.md?plain=1#L72-L78
  • Loading branch information
nicolasstucki committed Jun 16, 2023
1 parent 3b20d78 commit 1a5465e
Show file tree
Hide file tree
Showing 6 changed files with 125 additions and 25 deletions.
29 changes: 29 additions & 0 deletions compiler/src/dotty/tools/dotc/ast/Desugar.scala
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,35 @@ object desugar {
adaptToExpectedTpt(tree)
}

/** Split out the quoted pattern type variable definition from the pattern.
*
* Type variable definitions are all the `type t` defined at the start of a quoted pattern.
* Were name `t` is a pattern type variable name (i.e. lower case letters).
*
* ```
* type t1; ...; type tn; <pattern>
* ```
* is split into
* ```
* (List(<type t1>; ...; <type tn>), <pattern>)
* ```
*/
def quotedPatternTypeVariables(tree: untpd.Tree)(using Context): (List[untpd.TypeDef], untpd.Tree) =
tree match
case untpd.Block(stats, expr) =>
val untpdTypeVariables = stats.takeWhile {
case tdef @ untpd.TypeDef(name, _) => name.isVarPattern
case _ => false
}.asInstanceOf[List[untpd.TypeDef]]
val otherStats = stats.dropWhile {
case tdef @ untpd.TypeDef(name, _) => name.isVarPattern
case _ => false
}
val pattern = if otherStats.isEmpty then expr else untpd.cpy.Block(tree)(otherStats, expr)
(untpdTypeVariables, pattern)
case _ =>
(Nil, tree)

/** Add all evidence parameters in `params` as implicit parameters to `meth`.
* If the parameters of `meth` end in an implicit parameter list or using clause,
* evidence parameters are added in front of that list. Otherwise they are added
Expand Down
80 changes: 63 additions & 17 deletions compiler/src/dotty/tools/dotc/typer/QuotesAndSplices.scala
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,18 @@ import dotty.tools.dotc.transform.SymUtils._
import dotty.tools.dotc.typer.ErrorReporting.errorTree
import dotty.tools.dotc.typer.Implicits._
import dotty.tools.dotc.typer.Inferencing._
import dotty.tools.dotc.util.Property
import dotty.tools.dotc.util.Spans._
import dotty.tools.dotc.util.Stats.record
import dotty.tools.dotc.reporting.IllegalVariableInPatternAlternative
import scala.collection.mutable


/** Type quotes `'{ ... }` and splices `${ ... }` */
trait QuotesAndSplices {
self: Typer =>

import tpd._
import tpd.*
import QuotesAndSplices.*

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

private def checkSpliceOutsideQuote(tree: untpd.Tree)(using Context): Unit =
if (level == 0 && !ctx.owner.ownersIterator.exists(_.isInlineMethod))
Expand Down Expand Up @@ -385,11 +397,24 @@ trait QuotesAndSplices {
case Some(argPt: ValueType) => argPt // excludes TypeBounds
case _ => defn.AnyType
}
val quoted0 = desugar.quotedPattern(quoted, untpd.TypedSplice(TypeTree(quotedPt)))
val quoteCtx = quoteContext.addMode(Mode.QuotedPattern).retractMode(Mode.Pattern)
val quoted1 =
if quoted.isType then typedType(quoted0, WildcardType)(using quoteCtx)
else typedExpr(quoted0, WildcardType)(using quoteCtx)
val (untpdTypeVariables, quoted0) = desugar.quotedPatternTypeVariables(desugar.quotedPattern(quoted, untpd.TypedSplice(TypeTree(quotedPt))))

val (typeTypeVariables, patternCtx) =
val quoteCtx = quotePatternContext()
if untpdTypeVariables.isEmpty then (Nil, quoteCtx)
else typedBlockStats(untpdTypeVariables)(using quoteCtx)

val quoted1 = inContext(patternCtx) {
for typeVariable <- typeTypeVariables do
addQuotedPatternTypeVariable(typeVariable.symbol)

val pattern =
if quoted.isType then typedType(quoted0, WildcardType)
else typedExpr(quoted0, WildcardType)

if untpdTypeVariables.isEmpty then pattern
else tpd.Block(typeTypeVariables, pattern)
}

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

Expand Down Expand Up @@ -446,3 +471,24 @@ trait QuotesAndSplices {
proto = quoteClass.typeRef.appliedTo(replaceBindings(quoted1.tpe)))
}
}

object QuotesAndSplices {
import tpd._

/** Key for mapping from quoted pattern type variable names into their symbol */
private val TypeVariableKey = new Property.Key[collection.mutable.Map[TypeName, Symbol]]

/** Get the symbol for the quoted pattern type variable if it exists */
def getQuotedPatternTypeVariable(name: TypeName)(using Context): Option[Symbol] =
ctx.property(TypeVariableKey).get.get(name)

/** Get the symbol for the quoted pattern type variable if it exists */
def addQuotedPatternTypeVariable(sym: Symbol)(using Context): Unit =
ctx.property(TypeVariableKey).get.update(sym.name.asTypeName, sym)

/** Context used to type the contents of a quoted */
def quotePatternContext()(using Context): Context =
quoteContext.fresh.setNewScope
.addMode(Mode.QuotedPattern).retractMode(Mode.Pattern)
.setProperty(TypeVariableKey, collection.mutable.Map.empty)
}
18 changes: 11 additions & 7 deletions docs/_docs/reference/metaprogramming/macros.md
Original file line number Diff line number Diff line change
Expand Up @@ -504,18 +504,22 @@ def let(x: Expr[Any])(using Quotes): Expr[Any] =
let('{1}) // will return a `Expr[Any]` that contains an `Expr[Int]]`
```

It is also possible to refer to the same type variable multiple times in a pattern.

```scala
case '{ $x: (t, t) } =>
```

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.
We use the Scala backquote `` `t` `` naming convention which interprets the string within the backquote as a literal name identifier.
This is typically used when we have names that contain special characters that are not allowed for normal Scala identifiers.
But we use it to explicitly state that this is a reference to that name and not the introduction of a new variable.

```scala
case '{ type t; $x: `t` } =>
case '{ type t; $x: t } =>
```
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.

This is a bit more verbose but has some expressivity advantages such as allowing to define bounds on the variables.

```scala
case '{ type t >: List[Int] <: Seq[Int]; $x: `t` } =>
case '{ type t; $x: (`t`, `t`) } =>
case '{ type t >: List[Int] <: Seq[Int]; $x: t } =>
```


Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import scala.quoted.*

def foo(using Quotes): Unit =
(??? : Type[?]) match
case '[ (t, t, t) ] => // error // error
'{ ??? : Any } match
case '{ type u; $x: u } => // error
case '{ type u; ($ls: List[u]).map($f: u => Int) } => // error // error

2 changes: 1 addition & 1 deletion tests/neg-macros/quotedPatterns-5.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import scala.quoted.*
object Test {
def test(x: quoted.Expr[Int])(using Quotes): Unit = x match {
case '{ type t; 4 } => Type.of[t]
case '{ type t; poly[t]($x); 4 } => // error: duplicate pattern variable: t
case '{ type t; poly[t]($x); 4 } =>
case '{ type `t`; poly[`t`]($x); 4 } =>
Type.of[t] // error
case _ =>
Expand Down
12 changes: 12 additions & 0 deletions tests/pos-macros/quote-pattern-type-variable-no-escape.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import scala.quoted.*

def foo[T: Type](expr: Expr[Any])(using Quotes): Any =
expr match
case '{ $x: Map[t, t] } =>
case '{ type t; $x: Any } =>
case '{ type t; $x: Map[t, t] } =>
case '{ ($x: Set[t]).toSet[t] } =>

Type.of[T] match
case '[Map[t, t]] =>
case '[(t, t, t, t, t, t, t)] =>

0 comments on commit 1a5465e

Please sign in to comment.