From 5938d652c5e641413795829e9304ab41dd1ca362 Mon Sep 17 00:00:00 2001 From: Nicolas Stucki Date: Mon, 13 Feb 2023 14:46:01 +0100 Subject: [PATCH] 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 --- .../src/dotty/tools/dotc/ast/Desugar.scala | 10 +-- .../dotty/tools/dotc/parsing/Parsers.scala | 25 +++++++- .../tools/dotc/typer/QuotesAndSplices.scala | 15 ++++- .../_docs/reference/metaprogramming/macros.md | 13 +++- docs/_docs/reference/syntax.md | 4 +- .../sip-53-exprimental-b.scala | 8 +++ .../quote-pattern-type-var-bounds.scala | 22 +++++++ .../quote-type-variable-no-inference.check | 12 ++++ .../quote-type-variable-no-inference.scala | 8 +++ tests/pos-macros/i10864/Macro_1.scala | 15 +++++ tests/pos-macros/i10864/Test_2.scala | 4 ++ tests/pos-macros/i10864a/Macro_1.scala | 21 +++++++ tests/pos-macros/i10864a/Test_2.scala | 8 +++ tests/pos-macros/i11738.scala | 8 +++ tests/pos-macros/i7264.scala | 1 + .../pos-macros/multiline-quote-patterns.scala | 62 +++++++++++++++++++ 16 files changed, 223 insertions(+), 13 deletions(-) create mode 100644 tests/neg-custom-args/no-experimental/sip-53-exprimental-b.scala create mode 100644 tests/neg-macros/quote-pattern-type-var-bounds.scala create mode 100644 tests/neg-macros/quote-type-variable-no-inference.check create mode 100644 tests/neg-macros/quote-type-variable-no-inference.scala create mode 100644 tests/pos-macros/i10864/Macro_1.scala create mode 100644 tests/pos-macros/i10864/Test_2.scala create mode 100644 tests/pos-macros/i10864a/Macro_1.scala create mode 100644 tests/pos-macros/i10864a/Test_2.scala create mode 100644 tests/pos-macros/i11738.scala create mode 100644 tests/pos-macros/multiline-quote-patterns.scala diff --git a/compiler/src/dotty/tools/dotc/ast/Desugar.scala b/compiler/src/dotty/tools/dotc/ast/Desugar.scala index 1e09183573b3..e884e7282d1b 100644 --- a/compiler/src/dotty/tools/dotc/ast/Desugar.scala +++ b/compiler/src/dotty/tools/dotc/ast/Desugar.scala @@ -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; @@ -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) diff --git a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala index 3079b26df6cd..335ece005b72 100644 --- a/compiler/src/dotty/tools/dotc/parsing/Parsers.scala +++ b/compiler/src/dotty/tools/dotc/parsing/Parsers.scala @@ -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 @@ -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) } @@ -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 diff --git a/compiler/src/dotty/tools/dotc/typer/QuotesAndSplices.scala b/compiler/src/dotty/tools/dotc/typer/QuotesAndSplices.scala index 342b07c6026f..d3becfd6e9b7 100644 --- a/compiler/src/dotty/tools/dotc/typer/QuotesAndSplices.scala +++ b/compiler/src/dotty/tools/dotc/typer/QuotesAndSplices.scala @@ -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 = @@ -399,6 +399,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) @@ -409,7 +420,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 diff --git a/docs/_docs/reference/metaprogramming/macros.md b/docs/_docs/reference/metaprogramming/macros.md index 1ed89422eee5..87ab7d66d67d 100644 --- a/docs/_docs/reference/metaprogramming/macros.md +++ b/docs/_docs/reference/metaprogramming/macros.md @@ -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. diff --git a/docs/_docs/reference/syntax.md b/docs/_docs/reference/syntax.md index a705c5a3fd79..b4b3aab6b02f 100644 --- a/docs/_docs/reference/syntax.md +++ b/docs/_docs/reference/syntax.md @@ -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 @@ -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 diff --git a/tests/neg-custom-args/no-experimental/sip-53-exprimental-b.scala b/tests/neg-custom-args/no-experimental/sip-53-exprimental-b.scala new file mode 100644 index 000000000000..2ec597995bd1 --- /dev/null +++ b/tests/neg-custom-args/no-experimental/sip-53-exprimental-b.scala @@ -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 diff --git a/tests/neg-macros/quote-pattern-type-var-bounds.scala b/tests/neg-macros/quote-pattern-type-var-bounds.scala new file mode 100644 index 000000000000..b97b21552a1e --- /dev/null +++ b/tests/neg-macros/quote-pattern-type-var-bounds.scala @@ -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 +} diff --git a/tests/neg-macros/quote-type-variable-no-inference.check b/tests/neg-macros/quote-type-variable-no-inference.check new file mode 100644 index 000000000000..7e425e932117 --- /dev/null +++ b/tests/neg-macros/quote-type-variable-no-inference.check @@ -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` diff --git a/tests/neg-macros/quote-type-variable-no-inference.scala b/tests/neg-macros/quote-type-variable-no-inference.scala new file mode 100644 index 000000000000..de03f4445302 --- /dev/null +++ b/tests/neg-macros/quote-type-variable-no-inference.scala @@ -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] diff --git a/tests/pos-macros/i10864/Macro_1.scala b/tests/pos-macros/i10864/Macro_1.scala new file mode 100644 index 000000000000..7cf1e1850a76 --- /dev/null +++ b/tests/pos-macros/i10864/Macro_1.scala @@ -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] } +} diff --git a/tests/pos-macros/i10864/Test_2.scala b/tests/pos-macros/i10864/Test_2.scala new file mode 100644 index 000000000000..e93fa1302221 --- /dev/null +++ b/tests/pos-macros/i10864/Test_2.scala @@ -0,0 +1,4 @@ +def test = + T.run[List] + T.run[Map] + T.run[Tuple22] diff --git a/tests/pos-macros/i10864a/Macro_1.scala b/tests/pos-macros/i10864a/Macro_1.scala new file mode 100644 index 000000000000..67cac5f85abd --- /dev/null +++ b/tests/pos-macros/i10864a/Macro_1.scala @@ -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] } +} diff --git a/tests/pos-macros/i10864a/Test_2.scala b/tests/pos-macros/i10864a/Test_2.scala new file mode 100644 index 000000000000..7a1596d0fa41 --- /dev/null +++ b/tests/pos-macros/i10864a/Test_2.scala @@ -0,0 +1,8 @@ +@main +def run = + T.run[Int] + T.run[C] + T.run[List] + T.run[Map] + +class C[T <: Int] diff --git a/tests/pos-macros/i11738.scala b/tests/pos-macros/i11738.scala new file mode 100644 index 000000000000..e1213a5dee6d --- /dev/null +++ b/tests/pos-macros/i11738.scala @@ -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 _ => + '{()} diff --git a/tests/pos-macros/i7264.scala b/tests/pos-macros/i7264.scala index c87409561bee..82264402c768 100644 --- a/tests/pos-macros/i7264.scala +++ b/tests/pos-macros/i7264.scala @@ -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] ] => } } diff --git a/tests/pos-macros/multiline-quote-patterns.scala b/tests/pos-macros/multiline-quote-patterns.scala new file mode 100644 index 000000000000..a1f1649b6059 --- /dev/null +++ b/tests/pos-macros/multiline-quote-patterns.scala @@ -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] + } => +}