Skip to content

Commit

Permalink
Support type variable definitions in quoted patterns
Browse files Browse the repository at this point in the history
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 scala#10864
Fixes scala#11738
  • Loading branch information
nicolasstucki committed May 3, 2023
1 parent b87c16a commit 7cb8d05
Show file tree
Hide file tree
Showing 13 changed files with 198 additions and 8 deletions.
21 changes: 20 additions & 1 deletion compiler/src/dotty/tools/dotc/parsing/Parsers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1735,6 +1735,23 @@ object Parsers {
/** The block in a quote or splice */
def stagedBlock() = inBraces(block(simplify = true))

/** TypeBlock ::= {TypeBlockStat semi} Type
* TypeBlockStat ::= ‘type’ {nl} TypeDcl
*/
def typeBlock(): Tree =
quotePatternTypeVars() match
case Nil => typ()
case tDefs => Block(tDefs, typ())

def quotePatternTypeVars(): List[Tree] =
val tDefs = new ListBuffer[Tree]
while in.token == TYPE do
val mods = defAnnotsMods(BitSet())
tDefs += typeDefOrDcl(in.offset, in.skipToken(mods))
if in.token == SEMI then in.nextToken()
if in.isNewLine then in.nextToken()
tDefs.toList

/** ExprSplice ::= ‘$’ spliceId -- if inside quoted block
* | ‘$’ ‘{’ Block ‘}’ -- unless inside quoted pattern
* | ‘$’ ‘{’ Pattern ‘}’ -- when inside quoted pattern
Expand Down Expand Up @@ -2496,7 +2513,7 @@ object Parsers {
atSpan(in.skipToken()) {
withinStaged(StageKind.Quoted | (if (location.inPattern) StageKind.QuotedPattern else 0)) {
val body =
if (in.token == LBRACKET) inBrackets(typ())
if (in.token == LBRACKET) inBrackets(typeBlock())
else stagedBlock()
Quote(body)
}
Expand Down Expand Up @@ -3762,6 +3779,8 @@ object Parsers {
else makeTypeDef(bounds)
case SEMI | NEWLINE | NEWLINES | COMMA | RBRACE | OUTDENT | EOF =>
makeTypeDef(typeBounds())
case _ if (staged & StageKind.QuotedPattern) != 0 => // is in a quoted pattern
makeTypeDef(typeBounds())
case _ =>
syntaxErrorOrIncomplete(ExpectedTypeBoundOrEquals(in.token))
return EmptyTree // return to avoid setting the span to EmptyTree
Expand Down
19 changes: 15 additions & 4 deletions compiler/src/dotty/tools/dotc/typer/QuotesAndSplices.scala
Original file line number Diff line number Diff line change
Expand Up @@ -213,11 +213,11 @@ trait QuotesAndSplices {
* )
* ```
*/
private def splitQuotePattern(quoted: Tree)(using Context): (Map[Symbol, Bind], Tree, List[Tree]) = {
private def splitQuotePattern(quoted: Tree)(using Context): (Map[Symbol, Tree], Tree, List[Tree]) = {
val ctx0 = ctx

val typeBindings: collection.mutable.Map[Symbol, Bind] = collection.mutable.Map.empty
def getBinding(sym: Symbol): Bind =
val typeBindings: collection.mutable.Map[Symbol, Tree] = collection.mutable.Map.empty
def getBinding(sym: Symbol): Tree =
typeBindings.getOrElseUpdate(sym, {
val bindingBounds = sym.info
val bsym = newPatternBoundSymbol(sym.name.toString.stripPrefix("$").toTypeName, bindingBounds, quoted.span)
Expand Down Expand Up @@ -408,6 +408,17 @@ trait QuotesAndSplices {
}
val (untpdTypeVariables, quoted0) = desugar.quotedPatternTypeVariables(desugar.quotedPattern(quoted, untpd.TypedSplice(TypeTree(quotedPt))))

for untpdTypeVariable <- untpdTypeVariables do untpdTypeVariable.rhs match
case _: TypeBoundsTree => // ok
case LambdaTypeTree(_, body: TypeBoundsTree) => // ok
case _ => report.error("Quote type variable definition cannot be an alias", untpdTypeVariable.srcPos)

if quoted.isType && untpdTypeVariables.nonEmpty then
checkExperimentalFeature(
"explicit type variable declarations quoted type patterns (SIP-53)",
untpdTypeVariables.head.srcPos,
"\n\nSIP-53: https://docs.scala-lang.org/sips/quote-pattern-type-variable-syntax.html")

val (typeTypeVariables, patternCtx) =
val quoteCtx = quotePatternContext()
if untpdTypeVariables.isEmpty then (Nil, quoteCtx)
Expand All @@ -418,7 +429,7 @@ trait QuotesAndSplices {
addQuotedPatternTypeVariable(typeVariable.symbol)

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

if untpdTypeVariables.isEmpty then pattern
Expand Down
13 changes: 11 additions & 2 deletions docs/_docs/reference/metaprogramming/macros.md
Original file line number Diff line number Diff line change
Expand Up @@ -530,15 +530,24 @@ It works the same way as a quoted pattern but is restricted to contain a type.
Type variables can be used in quoted type patterns to extract a type.

```scala
def empty[T: Type]: Expr[T] =
def empty[T: Type](using Quotes): Expr[T] =
Type.of[T] match
case '[String] => '{ "" }
case '[List[t]] => '{ List.empty[t] }
case '[type t <: Option[Int]; List[t]] => '{ List.empty[t] }
...
```

`Type.of[T]` is used to summon the given instance of `Type[T]` in scope, it is equivalent to `summon[Type[T]]`.

It is possible to match against a higer-kinded type using appropriate type bounds on type variables.
```scala
def empty[K <: AnyKind : Type](using Quotes): Type[?] =
Type.of[K] match
case '[type f[X]; f] => Type.of[f]
case '[type f[X <: Int, Y]; f] => Type.of[f]
case '[type k <: AnyKind; k ] => Type.of[k]
```

#### Type testing and casting
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.
These cases will issue a warning at compile-time, but if they are ignored, they can result in unexpected behavior.
Expand Down
4 changes: 3 additions & 1 deletion docs/_docs/reference/syntax.md
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,7 @@ ColonArgument ::= colon [LambdaStart]
LambdaStart ::= FunParams (‘=>’ | ‘?=>’)
| HkTypeParamClause ‘=>’
Quoted ::= ‘'’ ‘{’ Block ‘}’
| ‘'’ ‘[’ Type ‘]’
| ‘'’ ‘[’ TypeBlock ‘]’
ExprSplice ::= spliceId -- if inside quoted block
| ‘$’ ‘{’ Block ‘}’ -- unless inside quoted pattern
| ‘$’ ‘{’ Pattern ‘}’ -- when inside quoted pattern
Expand All @@ -293,6 +293,8 @@ BlockStat ::= Import
| Extension
| Expr1
| EndMarker
TypeBlock ::= {TypeBlockStat semi} Type
TypeBlockStat ::= ‘type’ {nl} TypeDcl
ForExpr ::= ‘for’ ‘(’ Enumerators0 ‘)’ {nl} [‘do‘ | ‘yield’] Expr
| ‘for’ ‘{’ Enumerators0 ‘}’ {nl} [‘do‘ | ‘yield’] Expr
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import scala.quoted.*

def empty[K <: AnyKind : Type](using Quotes): Type[?] =
Type.of[K] match
case '[type t; `t`] => Type.of[t] // error
case '[type f[X]; `f`] => Type.of[f] // error
case '[type f[X <: Int, Y]; `f`] => Type.of[f] // error
case '[type k <: AnyKind; `k` ] => Type.of[k] // error
22 changes: 22 additions & 0 deletions tests/neg-macros/quote-pattern-type-var-bounds.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import scala.quoted.*
def types(t: Type[?])(using Quotes) = t match {
case '[ type t; Int ] =>
case '[ type t <: Int; Int ] =>
case '[ type t >: 1 <: Int; Int ] =>
case '[ type t = Int; Int ] => // error
case '[ type t = scala.Int; Int ] => // error
case '[ type f[t] <: List[Any]; Int ] =>
case '[ type f[t <: Int] <: List[Any]; Int ] =>
case '[ type f[t] = List[Any]; Int ] => // error
}

def expressions(x: Expr[Any])(using Quotes) = x match {
case '{ type t; () } =>
case '{ type t <: Int; () } =>
case '{ type t >: 1 <: Int; () } =>
case '{ type t = Int; () } => // error
case '{ type t = scala.Int; () } => // error
case '{ type f[t] <: List[Any]; () } =>
case '{ type f[t <: Int] <: List[Any]; () } =>
case '{ type f[t] = List[Any]; () } => // error
}
15 changes: 15 additions & 0 deletions tests/pos-macros/i10864/Macro_1.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import scala.quoted._

case class T(t: Type[_])

object T {
def impl[T <: AnyKind](using tt: Type[T])(using Quotes): Expr[Unit] = {
val t = T(tt)
t.t match
case '[type x <: AnyKind; x] => // ok
case _ => quotes.reflect.report.error("not ok :(")
'{}
}

inline def run[T <: AnyKind] = ${ impl[T] }
}
4 changes: 4 additions & 0 deletions tests/pos-macros/i10864/Test_2.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
def test =
T.run[List]
T.run[Map]
T.run[Tuple22]
21 changes: 21 additions & 0 deletions tests/pos-macros/i10864a/Macro_1.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import scala.quoted._

case class T(t: Type[_])

object T {
def impl[T <: AnyKind](using tt: Type[T])(using Quotes): Expr[Unit] = {
val t = T(tt)
t.t match
case '[type x; x] =>
assert(Type.show[x] == "scala.Int", Type.show[x])
case '[type f[X]; f] =>
assert(Type.show[f] == "[A >: scala.Nothing <: scala.Any] => scala.collection.immutable.List[A]", Type.show[f])
case '[type f[X <: Int]; f] =>
assert(Type.show[f] == "[T >: scala.Nothing <: scala.Int] => C[T]", Type.show[f])
case '[type f <: AnyKind; f] =>
assert(Type.show[f] == "[K >: scala.Nothing <: scala.Any, V >: scala.Nothing <: scala.Any] => scala.collection.immutable.Map[K, V]", Type.show[f])
'{}
}

inline def run[T <: AnyKind] = ${ impl[T] }
}
8 changes: 8 additions & 0 deletions tests/pos-macros/i10864a/Test_2.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
@main
def run =
T.run[Int]
T.run[C]
T.run[List]
T.run[Map]

class C[T <: Int]
8 changes: 8 additions & 0 deletions tests/pos-macros/i11738.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import scala.quoted.*

def blah[A](using Quotes, Type[A]): Expr[Unit] =
Type.of[A] match
case '[h *: t] => println(s"h = ${Type.show[h]}, t = ${Type.show[t]}") // ok
case '[type f[X]; f[a]] => println(s"f = ${Type.show[f]}, a = ${Type.show[a]}") // error
case _ =>
'{()}
1 change: 1 addition & 0 deletions tests/pos-macros/i7264.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@ class Foo {
def f[T2](t: Type[T2])(using Quotes) = t match {
case '[ *:[Int, t2] ] =>
Type.of[ *:[Int, t2] ]
case '[ type t <: Tuple; *:[t, t] ] =>
}
}
62 changes: 62 additions & 0 deletions tests/pos-macros/multiline-quote-patterns.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import scala.quoted.*
def types(t: Type[?])(using Quotes) = t match {
case '[
type t;
t
] =>

case '[
type t
t
] =>

case '[
type t
List[t]
] =>

case '[
type t;
type u;
Map[t, u]
] =>

case '[
type t
type u
Map[t, u]
] =>

case '[
type t; type u
t => u
] =>
}

def expressions(x: Expr[Any])(using Quotes) = x match {
case '{
type t;
$x: t
} =>

case '{
type t
$x: t
} =>

case '{
type t;
List()
} =>

case '{
type t
List()
} =>

case '{
type t
type u
Map.empty[t, u]
} =>
}

0 comments on commit 7cb8d05

Please sign in to comment.