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 #10864
Fixes #11738
  • Loading branch information
nicolasstucki committed May 31, 2023
1 parent 3958ee7 commit 6cb6ae3
Show file tree
Hide file tree
Showing 16 changed files with 226 additions and 16 deletions.
10 changes: 3 additions & 7 deletions compiler/src/dotty/tools/dotc/ast/Desugar.scala
Original file line number Diff line number Diff line change
Expand Up @@ -366,7 +366,7 @@ object desugar {
/** 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).
* Where name `t` is a pattern type variable name (i.e. lower case letters).
*
* ```
* type t1; ...; type tn; <pattern>
Expand All @@ -379,16 +379,12 @@ object desugar {
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 {
val (untpdTypeVariables, otherStats) = stats.span {
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)
(untpdTypeVariables.asInstanceOf[List[untpd.TypeDef]], pattern)
case _ =>
(Nil, tree)

Expand Down
25 changes: 24 additions & 1 deletion compiler/src/dotty/tools/dotc/parsing/Parsers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -1740,6 +1740,27 @@ object Parsers {
/** The block in a quote or splice */
def stagedBlock() = inBraces(block(simplify = true))

/** TypeBlock ::= {TypeBlockStat semi} Type
*/
def typeBlock(): Tree =
typeBlockStats() match
case Nil => typ()
case tdefs => Block(tdefs, typ())

def typeBlockStats(): List[Tree] =
val tdefs = new ListBuffer[Tree]
while in.token == TYPE do tdefs += typeBlockStat()
tdefs.toList

/** TypeBlockStat ::= ‘type’ {nl} TypeDcl
*/
def typeBlockStat(): Tree =
val mods = defAnnotsMods(BitSet())
val tdef = typeDefOrDcl(in.offset, in.skipToken(mods))
if in.token == SEMI then in.nextToken()
if in.isNewLine then in.nextToken()
tdef

/** ExprSplice ::= ‘$’ spliceId -- if inside quoted block
* | ‘$’ ‘{’ Block ‘}’ -- unless inside quoted pattern
* | ‘$’ ‘{’ Pattern ‘}’ -- when inside quoted pattern
Expand Down Expand Up @@ -2480,7 +2501,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, Nil)
}
Expand Down Expand Up @@ -3758,6 +3779,8 @@ object Parsers {
else makeTypeDef(bounds)
case SEMI | NEWLINE | NEWLINES | COMMA | RBRACE | OUTDENT | EOF =>
makeTypeDef(typeBounds())
case _ if (staged & StageKind.QuotedPattern) != 0 =>
makeTypeDef(typeBounds())
case _ =>
syntaxErrorOrIncomplete(ExpectedTypeBoundOrEquals(in.token))
return EmptyTree // return to avoid setting the span to EmptyTree
Expand Down
21 changes: 16 additions & 5 deletions compiler/src/dotty/tools/dotc/typer/QuotesAndSplices.scala
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ trait QuotesAndSplices {
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)
report.warning(em"Ignored bound$typeSymInfo\n\nConsider defining bounds explicitly `'{ $typeSym${typeSym.info & typeSymInfo}; ... }`", tree.srcPos)
ref(typeSym)
case None =>
def spliceOwner(ctx: Context): Symbol =
Expand Down Expand Up @@ -211,11 +211,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 @@ -393,6 +393,17 @@ trait QuotesAndSplices {
}
val (untpdTypeVariables, quoted0) = desugar.quotedPatternTypeVariables(desugar.quotedPattern(quoted, untpd.TypedSplice(TypeTree(quotedPt))))

for tdef @ untpd.TypeDef(_, rhs) <- untpdTypeVariables do rhs match
case _: TypeBoundsTree => // ok
case LambdaTypeTree(_, body: TypeBoundsTree) => // ok
case _ => report.error("Quote type variable definition cannot be an alias", tdef.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 @@ -403,7 +414,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 higher-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
}
12 changes: 12 additions & 0 deletions tests/neg-macros/quote-type-variable-no-inference.check
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
-- Warning: tests/neg-macros/quote-type-variable-no-inference.scala:5:17 -----------------------------------------------
5 | case '[ F[t, t] ] => // warn // error
| ^
| Ignored bound <: Double
|
| Consider defining bounds explicitly `'{ type t <: Int & Double; ... }`
-- [E057] Type Mismatch Error: tests/neg-macros/quote-type-variable-no-inference.scala:5:15 ----------------------------
5 | case '[ F[t, t] ] => // warn // error
| ^
| Type argument t does not conform to upper bound Double
|
| longer explanation available when compiling with `-explain`
8 changes: 8 additions & 0 deletions tests/neg-macros/quote-type-variable-no-inference.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import scala.quoted.*

def test(x: Type[?])(using Quotes) =
x match
case '[ F[t, t] ] => // warn // error
case '[ type u <: Int & Double; F[u, u] ] =>

type F[x <: Int, y <: Double]
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 6cb6ae3

Please sign in to comment.